diff --git a/Cargo.lock b/Cargo.lock
index 1c580f3280..68db479959 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -814,6 +814,15 @@ dependencies = [
"dirs-sys",
]
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys",
+]
+
[[package]]
name = "dirs-sys"
version = "0.3.7"
@@ -2529,6 +2538,15 @@ dependencies = [
"opaque-debug 0.3.0",
]
+[[package]]
+name = "shellexpand"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd1c7ddea665294d484c39fd0c0d2b7e35bbfe10035c5fe1854741a57f6880e1"
+dependencies = [
+ "dirs 4.0.0",
+]
+
[[package]]
name = "signal-hook"
version = "0.1.17"
@@ -2880,7 +2898,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76971977e6121664ec1b960d1313aacfa75642adc93b9d4d53b247bd4cb1747e"
dependencies = [
- "dirs",
+ "dirs 2.0.2",
"fnv",
"nom 5.1.2",
"phf 0.8.0",
@@ -4056,6 +4074,7 @@ dependencies = [
"rmp-serde",
"serde",
"serde_json",
+ "shellexpand",
"signal-hook 0.3.14",
"strip-ansi-escapes",
"strum",
diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml
index 76ca72c90c..6c29f1e435 100644
--- a/zellij-utils/Cargo.toml
+++ b/zellij-utils/Cargo.toml
@@ -38,6 +38,7 @@ miette = { version = "3.3.0", features = ["fancy"] }
regex = "1.5.5"
tempfile = "3.2.0"
kdl = { version = "4.5.0", features = ["span"] }
+shellexpand = "3.0.0"
#[cfg(not(target_family = "wasm"))]
[target.'cfg(not(target_family = "wasm"))'.dependencies]
diff --git a/zellij-utils/src/input/unit/layout_test.rs b/zellij-utils/src/input/unit/layout_test.rs
index a38abe0287..fee88aae44 100644
--- a/zellij-utils/src/input/unit/layout_test.rs
+++ b/zellij-utils/src/input/unit/layout_test.rs
@@ -2071,3 +2071,60 @@ fn run_plugin_location_parsing() {
};
assert_eq!(layout, expected_layout);
}
+
+#[test]
+fn env_var_expansion() {
+ let raw_layout = r#"
+ layout {
+ // cwd tests + composition
+ cwd "$TEST_GLOBAL_CWD"
+ pane cwd="relative" // -> /abs/path/relative
+ pane cwd="/another/abs" // -> /another/abs
+ pane cwd="$TEST_LOCAL_CWD" // -> /another/abs
+ pane cwd="$TEST_RELATIVE" // -> /abs/path/relative
+ pane command="ls" cwd="$TEST_ABSOLUTE" // -> /somewhere
+ pane edit="file.rs" cwd="$TEST_ABSOLUTE" // -> /somewhere/file.rs
+ pane edit="file.rs" cwd="~/backup" // -> /home/aram/backup/file.rs
+
+ // other paths
+ pane command="~/backup/executable" // -> /home/aram/backup/executable
+ pane edit="~/backup/foo.txt" // -> /home/aram/backup/foo.txt
+ }
+ "#;
+ let env_vars = [
+ ("TEST_GLOBAL_CWD", "/abs/path"),
+ ("TEST_LOCAL_CWD", "/another/abs"),
+ ("TEST_RELATIVE", "relative"),
+ ("TEST_ABSOLUTE", "/somewhere"),
+ ("HOME", "/home/aram"),
+ ];
+ let mut old_vars = Vec::new();
+ // set environment variables for test, keeping track of existing values.
+ for (key, value) in env_vars {
+ old_vars.push((key, std::env::var(key).ok()));
+ std::env::set_var(key, value);
+ }
+ let layout = Layout::from_kdl(raw_layout, "layout_file_name".into(), None, None);
+ // restore environment.
+ for (key, opt) in old_vars {
+ match opt {
+ Some(value) => std::env::set_var(key, &value),
+ None => std::env::remove_var(key),
+ }
+ }
+ let layout = layout.unwrap();
+ assert_snapshot!(format!("{layout:#?}"));
+}
+
+#[test]
+fn env_var_missing() {
+ std::env::remove_var("SOME_UNIQUE_VALUE");
+ let kdl_layout = r#"
+ layout {
+ cwd "$SOME_UNIQUE_VALUE"
+ pane cwd="relative"
+ }
+ "#;
+ let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None, None);
+ assert!(layout.is_err(), "invalid env var lookup should fail");
+}
diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__env_var_expansion.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__env_var_expansion.snap
new file mode 100644
index 0000000000..de30d22961
--- /dev/null
+++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__env_var_expansion.snap
@@ -0,0 +1,212 @@
+---
+source: zellij-utils/src/input/./unit/layout_test.rs
+assertion_line: 2116
+expression: "format!(\"{layout:#?}\")"
+---
+Layout {
+ tabs: [],
+ focused_tab_index: None,
+ template: Some(
+ (
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Cwd(
+ "/abs/path/relative",
+ ),
+ ),
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Cwd(
+ "/another/abs",
+ ),
+ ),
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Cwd(
+ "/another/abs",
+ ),
+ ),
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Cwd(
+ "/abs/path/relative",
+ ),
+ ),
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Command(
+ RunCommand {
+ command: "ls",
+ args: [],
+ cwd: Some(
+ "/somewhere",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ },
+ ),
+ ),
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ EditFile(
+ "/somewhere/file.rs",
+ None,
+ Some(
+ "/somewhere",
+ ),
+ ),
+ ),
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ EditFile(
+ "/home/aram/backup/file.rs",
+ None,
+ Some(
+ "/home/aram/backup",
+ ),
+ ),
+ ),
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ Command(
+ RunCommand {
+ command: "/home/aram/backup/executable",
+ args: [],
+ cwd: Some(
+ "/abs/path",
+ ),
+ hold_on_close: true,
+ hold_on_start: false,
+ },
+ ),
+ ),
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: Some(
+ EditFile(
+ "/home/aram/backup/foo.txt",
+ None,
+ Some(
+ "/abs/path",
+ ),
+ ),
+ ),
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: false,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ },
+ [],
+ ),
+ ),
+ swap_layouts: [],
+ swap_tiled_layouts: [],
+ swap_floating_layouts: [],
+}
diff --git a/zellij-utils/src/kdl/kdl_layout_parser.rs b/zellij-utils/src/kdl/kdl_layout_parser.rs
index 9af0561de7..21441cf173 100644
--- a/zellij-utils/src/kdl/kdl_layout_parser.rs
+++ b/zellij-utils/src/kdl/kdl_layout_parser.rs
@@ -333,22 +333,27 @@ impl<'a> KdlLayoutParser<'a> {
(None, None) => None,
})
}
- fn parse_cwd(&self, kdl_node: &KdlNode) -> Result