aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-utils/src/kdl
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-06-10 16:50:55 +0200
committerGitHub <noreply@github.com>2026-06-10 16:50:55 +0200
commitb6a5ad0e5a93d9f584b3f7a0fc90f497a09cbdb0 (patch)
tree38c77a381cfb937b428752a588c09a45c81434c9 /zellij-utils/src/kdl
parentfeat: PWA support for the web client (#5184) (diff)
downloadsl0ppij-b6a5ad0e5a93d9f584b3f7a0fc90f497a09cbdb0.tar.gz
sl0ppij-b6a5ad0e5a93d9f584b3f7a0fc90f497a09cbdb0.tar.xz
sl0ppij-b6a5ad0e5a93d9f584b3f7a0fc90f497a09cbdb0.zip
Mobile layout (#5241)
* phase 2+3 * initial mobile plugin (phases 4 + 5 + 6) * some bugfixes * implement stage 7 * mobile interface improvements * improve mobile pane/tab picker * more ui improvements and fixes * various mobile fixes * some mobile fixes * mobile keyboard triggering fix * some cleanups * initial plugin keyboard implementation * improve keyboard hit zones * improve keyboard * pinch resize * pinching fixes * implement scrolling + panning * initial keyboard resizing * fix: mobile first render issues * keyboard responsiveness * fix some issues with the keyboard * implement fit * fix mobile scrolling * add tilde cell to compact * improve mobile keyboard * fix starting mobile terminal font to natural keyboard size * fix mobile resize blank screen flash * shadow focus for mobile * more keyboard fixes * improve mobile status bar * fix: render plugins to mobile clients * allow opening new panes/tabs from the mobile plugin * allow configuring * fix dot slash * improve mobile native soft keyboard interaction * switch to native keyboard - initial implementation * dirty hacks, but phone keyboard works now * adjust keyboard help strip * fix cursor in mobile * fix keyboard and help line * rename keyboard to modifier bar * allow starting new session * make clicking session name bring us to new session menu * allow switching to desktop from the mobile ui * mobile welcome screen initial implementation * mobile welcome screen fixes * various welcome screen fixes * more mobile fixes * moar mobile fixes * mobile ui redesign * fix: swallowed backspace bug * switch session screen improvement * touch up some interfaces * refactor: move mobile state to own struct * refactor: move fit resizing to the server * remove dead flash code * some cleanups * refactor: clear fit on close * refactor: improve fit api * refactor: pane sync functions * refactor: mouse * refactor: break into modules * some cleanups for the sessions screen * fix scrolling and other stuff * refactor: sessions screen * refactor: panes screen * refactor: new session screen * refactor: menu screen * refactor: render * cleanups * refactor: plugin structure * remove unused welcome screen path * Rename api * refactor input.js * cleanup useless comments * fit/mobile refactoring * take out the garbage * remove more comments * remove mobile flash on startup/attach * fix: various mobile startup issues * fix space bugs * cleanup * rustfmt * fix windows build * adjust modifier bar ui * add pr
Diffstat (limited to 'zellij-utils/src/kdl')
-rw-r--r--zellij-utils/src/kdl/mod.rs175
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap1
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap18
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string.snap3
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap17
5 files changed, 212 insertions, 2 deletions
diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs
index c7cc5366..0164ef92 100644
--- a/zellij-utils/src/kdl/mod.rs
+++ b/zellij-utils/src/kdl/mod.rs
@@ -81,6 +81,7 @@ macro_rules! parse_kdl_action_arguments {
"Confirm" => Ok(Action::Confirm),
"Deny" => Ok(Action::Deny),
"ToggleMouseMode" => Ok(Action::ToggleMouseMode),
+ "ToggleMobileMode" => Ok(Action::ToggleMobileMode),
"PreviousSwapLayout" => Ok(Action::PreviousSwapLayout),
"NextSwapLayout" => Ok(Action::NextSwapLayout),
"Clear" => Ok(Action::ClearScreen),
@@ -1240,6 +1241,7 @@ impl Action {
Some(node)
},
Action::ToggleMouseMode => Some(KdlNode::new("ToggleMouseMode")),
+ Action::ToggleMobileMode => Some(KdlNode::new("ToggleMobileMode")),
Action::PreviousSwapLayout => Some(KdlNode::new("PreviousSwapLayout")),
Action::NextSwapLayout => Some(KdlNode::new("NextSwapLayout")),
Action::BreakPane => Some(KdlNode::new("BreakPane")),
@@ -1582,6 +1584,9 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
"ToggleMouseMode" => {
parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
},
+ "ToggleMobileMode" => {
+ parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
+ },
"Detach" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
"SwitchSession" => {
let name = kdl_get_string_property_or_child_value!(kdl_action, "name")
@@ -2824,6 +2829,39 @@ impl Options {
let mouse_click_through =
kdl_property_first_arg_as_bool_or_error!(kdl_options, "mouse_click_through")
.map(|(v, _)| v);
+ let mobile_layout =
+ match kdl_property_first_arg_as_string_or_error!(kdl_options, "mobile_layout") {
+ Some((value, entry)) => {
+ use crate::input::options::MobileLayoutConfiguration;
+ match value.parse::<MobileLayoutConfiguration>() {
+ Ok(v) => Some(v),
+ Err(e) => return Err(kdl_parsing_error!(e, entry)),
+ }
+ },
+ None => None,
+ };
+ let mobile_threshold_cols =
+ match kdl_property_first_arg_as_i64_or_error!(kdl_options, "mobile_threshold_cols") {
+ Some((value, _)) if value >= 0 => Some(value as u16),
+ Some((value, entry)) => {
+ return Err(kdl_parsing_error!(
+ format!("mobile_threshold_cols must be >= 0, found '{}'", value),
+ entry
+ ));
+ },
+ None => None,
+ };
+ let mobile_threshold_rows =
+ match kdl_property_first_arg_as_i64_or_error!(kdl_options, "mobile_threshold_rows") {
+ Some((value, _)) if value >= 0 => Some(value as u16),
+ Some((value, entry)) => {
+ return Err(kdl_parsing_error!(
+ format!("mobile_threshold_rows must be >= 0, found '{}'", value),
+ entry
+ ));
+ },
+ None => None,
+ };
Ok(Options {
simplified_ui,
@@ -2873,6 +2911,9 @@ impl Options {
enforce_https_for_localhost,
post_command_discovery_hook,
client_async_worker_tasks,
+ mobile_layout,
+ mobile_threshold_cols,
+ mobile_threshold_rows,
})
}
pub fn from_string(stringified_keybindings: &String) -> Result<Self, ConfigError> {
@@ -4218,6 +4259,94 @@ impl Options {
None
}
}
+ fn mobile_layout_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
+ use crate::input::options::MobileLayoutConfiguration;
+ let comment_text = format!(
+ "{}\n{}\n{}\n{}\n{}\n{}\n{}",
+ " ",
+ "// When attached clients land in the mobile UI plugin.",
+ "// Options:",
+ "// - \"web\" (Default — web clients only, gated on mobile_threshold_cols/rows)",
+ "// - \"always\" (any client, gated on mobile_threshold_cols/rows)",
+ "// - \"never\"",
+ "// ",
+ );
+ let create_node = |value: MobileLayoutConfiguration| -> KdlNode {
+ let mut node = KdlNode::new("mobile_layout");
+ let s = match value {
+ MobileLayoutConfiguration::Web => "web",
+ MobileLayoutConfiguration::Always => "always",
+ MobileLayoutConfiguration::Never => "never",
+ };
+ node.push(KdlValue::String(s.to_string()));
+ node
+ };
+ if let Some(value) = self.mobile_layout {
+ let mut node = create_node(value);
+ if add_comments {
+ node.set_leading(format!("{}\n", comment_text));
+ }
+ Some(node)
+ } else if add_comments {
+ let mut node = create_node(MobileLayoutConfiguration::Web);
+ node.set_leading(format!("{}\n// ", comment_text));
+ Some(node)
+ } else {
+ None
+ }
+ }
+ fn mobile_threshold_cols_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
+ let comment_text = format!(
+ "{}\n{}\n{}",
+ " ",
+ "// Column breakpoint for mobile_layout (web/always). 0 = always match.",
+ "// Default: 60",
+ );
+ let create_node = |value: u16| -> KdlNode {
+ let mut node = KdlNode::new("mobile_threshold_cols");
+ node.push(KdlValue::Base10(value as i64));
+ node
+ };
+ if let Some(value) = self.mobile_threshold_cols {
+ let mut node = create_node(value);
+ if add_comments {
+ node.set_leading(format!("{}\n", comment_text));
+ }
+ Some(node)
+ } else if add_comments {
+ let mut node = create_node(60);
+ node.set_leading(format!("{}\n// ", comment_text));
+ Some(node)
+ } else {
+ None
+ }
+ }
+ fn mobile_threshold_rows_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
+ let comment_text = format!(
+ "{}\n{}\n{}",
+ " ",
+ "// Row breakpoint for mobile_layout (web/always). 0 = always match.",
+ "// Default: 30",
+ );
+ let create_node = |value: u16| -> KdlNode {
+ let mut node = KdlNode::new("mobile_threshold_rows");
+ node.push(KdlValue::Base10(value as i64));
+ node
+ };
+ if let Some(value) = self.mobile_threshold_rows {
+ let mut node = create_node(value);
+ if add_comments {
+ node.set_leading(format!("{}\n", comment_text));
+ }
+ Some(node)
+ } else if add_comments {
+ let mut node = create_node(30);
+ node.set_leading(format!("{}\n// ", comment_text));
+ Some(node)
+ } else {
+ None
+ }
+ }
fn client_async_worker_tasks_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
let comment_text = r#"
// Number of async worker tasks to spawn per active client.
@@ -4396,6 +4525,15 @@ impl Options {
{
nodes.push(client_async_worker_tasks);
}
+ if let Some(mobile_layout) = self.mobile_layout_to_kdl(add_comments) {
+ nodes.push(mobile_layout);
+ }
+ if let Some(mobile_threshold_cols) = self.mobile_threshold_cols_to_kdl(add_comments) {
+ nodes.push(mobile_threshold_cols);
+ }
+ if let Some(mobile_threshold_rows) = self.mobile_threshold_rows_to_kdl(add_comments) {
+ nodes.push(mobile_threshold_rows);
+ }
nodes
}
}
@@ -7111,6 +7249,9 @@ fn config_options_to_string() {
support_kitty_keyboard_protocol false
web_server true
web_sharing "disabled"
+ mobile_layout "always"
+ mobile_threshold_cols 72
+ mobile_threshold_rows 0
"##;
let document: KdlDocument = fake_config.parse().unwrap();
let deserialized = Options::from_kdl(&document).unwrap();
@@ -7158,6 +7299,9 @@ fn config_options_to_string_with_comments() {
support_kitty_keyboard_protocol false
web_server true
web_sharing "disabled"
+ mobile_layout "always"
+ mobile_threshold_cols 72
+ mobile_threshold_rows 0
"##;
let document: KdlDocument = fake_config.parse().unwrap();
let deserialized = Options::from_kdl(&document).unwrap();
@@ -7192,6 +7336,37 @@ fn config_options_to_string_without_options() {
}
#[test]
+fn mobile_layout_kdl_round_trip_for_every_variant() {
+ use crate::input::options::MobileLayoutConfiguration;
+ let cases = [
+ ("web", MobileLayoutConfiguration::Web, (60, 30)),
+ ("always", MobileLayoutConfiguration::Always, (0, 0)),
+ ("never", MobileLayoutConfiguration::Never, (40, 0)),
+ ];
+ for (value, expected, (cols, rows)) in cases {
+ let fake_config = format!(
+ r##"
+ mobile_layout "{value}"
+ mobile_threshold_cols {cols}
+ mobile_threshold_rows {rows}
+ "##
+ );
+ let document: KdlDocument = fake_config.parse().unwrap();
+ let parsed = Options::from_kdl(&document).unwrap();
+ assert_eq!(parsed.mobile_layout, Some(expected), "case: {value}");
+ assert_eq!(parsed.mobile_threshold_cols, Some(cols), "case: {value}");
+ assert_eq!(parsed.mobile_threshold_rows, Some(rows), "case: {value}");
+
+ let mut serialized = Options::to_kdl(&parsed, false);
+ let mut fake_document = KdlDocument::new();
+ fake_document.nodes_mut().append(&mut serialized);
+ let reparsed =
+ Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap();
+ assert_eq!(parsed, reparsed, "round-trip mismatch for {value}");
+ }
+}
+
+#[test]
fn config_options_to_string_with_some_options() {
let fake_config = r##"
default_layout "compact"
diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap
index 6eda59b9..1ab39935 100644
--- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap
+++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap
@@ -261,6 +261,7 @@ plugins {
filepicker location="zellij:strider" {
cwd "/"
}
+ mobile location="zellij:mobile"
plugin-manager location="zellij:plugin-manager"
session-manager location="zellij:session-manager"
status-bar location="zellij:status-bar"
diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap
index c49fa8f9..5dbede33 100644
--- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap
+++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap
@@ -264,6 +264,7 @@ plugins {
filepicker location="zellij:strider" {
cwd "/"
}
+ mobile location="zellij:mobile"
plugin-manager location="zellij:plugin-manager"
session-manager location="zellij:session-manager"
status-bar location="zellij:status-bar"
@@ -582,4 +583,19 @@ web_client {
// typically work best. Set to 0 to use the number of (physical) CPU cores.
// Note: This only applies to web clients at the moment.
// client_async_worker_tasks 4
-
+
+// When attached clients land in the mobile UI plugin.
+// Options:
+// - "web" (Default — web clients only, gated on mobile_threshold_cols/rows)
+// - "always" (any client, gated on mobile_threshold_cols/rows)
+// - "never"
+//
+// mobile_layout "web"
+
+// Column breakpoint for mobile_layout (web/always). 0 = always match.
+// Default: 60
+// mobile_threshold_cols 60
+
+// Row breakpoint for mobile_layout (web/always). 0 = always match.
+// Default: 30
+// mobile_threshold_rows 30
diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string.snap
index 6c78ea96..43c82f7e 100644
--- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string.snap
+++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string.snap
@@ -31,3 +31,6 @@ disable_session_metadata true
support_kitty_keyboard_protocol false
web_server true
web_sharing "disabled"
+mobile_layout "always"
+mobile_threshold_cols 72
+mobile_threshold_rows 0
diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap
index eda40849..347a2074 100644
--- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap
+++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap
@@ -301,4 +301,19 @@ web_sharing "disabled"
// typically work best. Set to 0 to use the number of (physical) CPU cores.
// Note: This only applies to web clients at the moment.
// client_async_worker_tasks 4
-
+
+// When attached clients land in the mobile UI plugin.
+// Options:
+// - "web" (Default — web clients only, gated on mobile_threshold_cols/rows)
+// - "always" (any client, gated on mobile_threshold_cols/rows)
+// - "never"
+//
+mobile_layout "always"
+
+// Column breakpoint for mobile_layout (web/always). 0 = always match.
+// Default: 60
+mobile_threshold_cols 72
+
+// Row breakpoint for mobile_layout (web/always). 0 = always match.
+// Default: 30
+mobile_threshold_rows 0