aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-utils/src/kdl
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-12 18:44:24 +0200
committeruser <user@clank>2026-08-12 18:44:24 +0200
commit2c67579398835d51e625085ce3b22c22bc8f0cba (patch)
tree2b22d137d988f0bbf658ec4d37336f813933bfa1 /zellij-utils/src/kdl
parentsl0p: document workspace hints for consumers (diff)
downloadsl0ppij-2c67579398835d51e625085ce3b22c22bc8f0cba.tar.gz
sl0ppij-2c67579398835d51e625085ce3b22c22bc8f0cba.tar.xz
sl0ppij-2c67579398835d51e625085ce3b22c22bc8f0cba.zip
sl0p: --purpose flag and tab-level purpose
Completes the two pieces deferred from the first workspace-hints pass. `zellij run --purpose X` / `zellij action new-pane --purpose X` tag a pane at spawn time, which is what an agent wants for panes it creates itself; the OSC wrapper still works for programs that declare their own role. Not supported with --in-place, which replaces another pane rather than being a pane of its own: clap rejects the combination rather than accepting a flag that would do nothing. `tab name="work" purpose="agent-work"` gives a whole tab a role, reported by `list-tabs --json`. It rides the tab's ROOT layout node, reusing the pane field because a tab's root is the tab container and never itself a pane -- asserted by a test for the tightest case (a tab holding exactly one pane), since a leak there would silently mislabel a pane. The interesting part is why the flag first shipped broken. There are TWO protobuf boundaries -- the plugin API and the client->server IPC contract -- and a CLI flag must cross the second one. Defaulting the field in the conversion (what "no protoc" usually forces) compiles cleanly and then silently drops the value in transit, so --purpose parsed, printed a pane id, and did nothing. Fixed by adding the field to both contracts by hand, in the .proto AND the committed generated prost, kept in lockstep so a future regeneration reproduces it: protoc is only needed to regenerate, not to build. Both boundaries now have a round-trip test, and the IPC one was confirmed to fail when the wire read is removed. Sanitising moved to Grid::set_authoritative_purpose, the single choke point every out-of-band source passes through, after the CLI path was observed delivering `bad purpose; rm -rf /` intact into list-panes --json.
Diffstat (limited to 'zellij-utils/src/kdl')
-rw-r--r--zellij-utils/src/kdl/kdl_layout_parser.rs7
-rw-r--r--zellij-utils/src/kdl/mod.rs7
2 files changed, 14 insertions, 0 deletions
diff --git a/zellij-utils/src/kdl/kdl_layout_parser.rs b/zellij-utils/src/kdl/kdl_layout_parser.rs
index 9e823849..41b4a430 100644
--- a/zellij-utils/src/kdl/kdl_layout_parser.rs
+++ b/zellij-utils/src/kdl/kdl_layout_parser.rs
@@ -145,6 +145,7 @@ impl<'a> KdlLayoutParser<'a> {
|| property_name == "min_panes"
|| property_name == "exact_panes"
|| property_name == "hide_floating_panes"
+ || property_name == "purpose"
}
pub fn is_a_reserved_plugin_property(property_name: &str) -> bool {
property_name == "location"
@@ -1261,10 +1262,16 @@ impl<'a> KdlLayoutParser<'a> {
},
None => vec![],
};
+ // Tab-level purpose rides the tab's ROOT layout, which is the tab
+ // container and never itself a pane (asserted by
+ // sl0p_tab_root_layout_is_never_itself_a_pane).
+ let tab_purpose = kdl_get_string_property_or_child_value!(kdl_node, "purpose")
+ .and_then(|s| crate::input::layout::sanitize_pane_purpose(s));
let mut pane_layout = TiledPaneLayout {
children_split_direction,
children,
hide_floating_panes,
+ purpose: tab_purpose,
..Default::default()
};
if let Some(cwd_prefix) = &self.cwd_prefix(tab_cwd.as_ref())? {
diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs
index 358fa3e2..98743d67 100644
--- a/zellij-utils/src/kdl/mod.rs
+++ b/zellij-utils/src/kdl/mod.rs
@@ -576,6 +576,7 @@ impl Action {
});
} else if string == "stacked" {
return Ok(Action::NewStackedPane {
+ purpose: None,
command: None,
pane_name: None,
near_current_pane: false,
@@ -2067,6 +2068,7 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
.and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "borderless"));
if floating {
Ok(Action::NewFloatingPane {
+ purpose: None,
command: Some(run_command_action),
pane_name: name,
coordinates: FloatingPaneCoordinates::new(
@@ -2088,6 +2090,7 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
})
} else if stacked {
Ok(Action::NewStackedPane {
+ purpose: None,
command: Some(run_command_action),
pane_name: name,
near_current_pane: false,
@@ -2096,6 +2099,7 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
})
} else {
Ok(Action::NewTiledPane {
+ purpose: None,
direction,
command: Some(run_command_action),
pane_name: name,
@@ -6521,6 +6525,7 @@ impl TabInfo {
optional_int_node!("selectable_floating_panes_count", usize).unwrap_or(0);
let tab_id = optional_int_node!("tab_id", usize).unwrap_or(0);
Ok(TabInfo {
+ purpose: None,
position,
name,
active,
@@ -6960,6 +6965,7 @@ fn serialize_and_deserialize_session_info_with_data() {
name: "my session name".to_owned(),
tabs: vec![
TabInfo {
+ purpose: None,
position: 0,
name: "tab 1".to_owned(),
active: true,
@@ -6981,6 +6987,7 @@ fn serialize_and_deserialize_session_info_with_data() {
has_bell_notification: false,
},
TabInfo {
+ purpose: None,
position: 1,
name: "tab 2".to_owned(),
active: true,