//! The layout system. // Layouts have been moved from [`zellij-server`] to // [`zellij-utils`] in order to provide more helpful // error messages to the user until a more general // logging system is in place. // In case there is a logging system in place evaluate, // if [`zellij-utils`], or [`zellij-server`] is a proper // place. // If plugins should be able to depend on the layout system // then [`zellij-utils`] could be a proper place. #[cfg(not(target_family = "wasm"))] use crate::downloader::Downloader; use crate::{ data::{Direction, LayoutInfo, LayoutMetadata, LayoutParsingError, LayoutWithError}, home::{default_layout_dir, find_default_config_dir}, input::{ command::RunCommand, config::{Config, ConfigError}, }, pane_size::{Constraint, Dimension, PaneGeom}, setup::{self}, }; use std::cmp::Ordering; use std::fmt::{Display, Formatter}; use std::str::FromStr; use super::plugins::{PluginAliases, PluginTag, PluginsConfigError}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::vec::Vec; use std::{ fmt, ops::Not, path::{Path, PathBuf}, }; use std::{fs::File, io::prelude::*}; use url::Url; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy)] pub enum SplitDirection { Horizontal, Vertical, } impl Not for SplitDirection { type Output = Self; fn not(self) -> Self::Output { match self { SplitDirection::Horizontal => SplitDirection::Vertical, SplitDirection::Vertical => SplitDirection::Horizontal, } } } impl From for SplitDirection { fn from(direction: Direction) -> Self { match direction { Direction::Left | Direction::Right => SplitDirection::Horizontal, Direction::Down | Direction::Up => SplitDirection::Vertical, } } } /// Longest status bar text we keep. A pane can only ever show one row of it, and /// the string is held for the life of the pane, so there is no reason to retain /// megabytes because a program looped. const MAX_STATUS_BAR_TEXT: usize = 512; /// Make program-supplied status text safe to draw. /// /// The payload arrives in-band from whatever is running in the pane -- which /// includes `cat` of a file someone else wrote -- so it is untrusted. Control /// characters are dropped rather than escaped: a stray ESC would let the payload /// break out of the frame row and repaint anything it liked, and a newline or /// carriage return would corrupt the row. Truncation is by characters, not bytes, /// so it cannot split a multi-byte one. pub fn sanitize_status_bar_text(raw: &str) -> String { raw.chars() .filter(|c| !c.is_control()) .take(MAX_STATUS_BAR_TEXT) .collect::() .trim() .to_owned() } /// Longest pane purpose we accept. A purpose is a vocabulary token, not prose. const MAX_PANE_PURPOSE: usize = 64; /// Normalise a pane `purpose` (see `sl0p/workspace-hints-proposal.md`). /// /// Zellij deliberately does not know what any purpose MEANS -- the consuming /// agent owns the whole vocabulary. All that is enforced here is that the value /// is a safe, bounded token, because it arrives from two untrusted-ish places: a /// layout file shipped in a repository, and an OSC sequence any program in the /// pane can emit. /// /// Restricted to `[A-Za-z0-9_.:/-]` rather than merely stripping control /// characters, because unlike status bar text a purpose is never rendered -- it /// is matched against and pasted into consumers' shell/JSON pipelines, where /// whitespace, quotes and `;` are what cause trouble. Returns None for anything /// that sanitises down to nothing, so "absent" and "empty" cannot diverge. pub fn sanitize_pane_purpose(raw: &str) -> Option { let cleaned: String = raw .chars() .filter(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | ':' | '/')) .take(MAX_PANE_PURPOSE) .collect(); if cleaned.is_empty() { None } else { Some(cleaned) } } /// A clickable button in a pane's status bar (OSC 5577). #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct StatusBarButton { /// Echoed back to the program when clicked. Restricted to /// `[A-Za-z0-9_-]` so a hostile payload cannot smuggle arbitrary text into /// the report we write to a program's stdin. pub id: String, pub label: String, } /// Longest id we accept. Ids are opaque handles, not prose. const MAX_STATUS_BAR_ID: usize = 32; /// Most buttons we accept. A frame row cannot show more than a handful, and the /// list is retained for the life of the pane. const MAX_STATUS_BAR_BUTTONS: usize = 8; fn is_valid_status_bar_id(id: &str) -> bool { !id.is_empty() && id.len() <= MAX_STATUS_BAR_ID && id .chars() .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') } /// Decode one `%`-escaped wire field. /// /// Labels are free text but the wire format is `;`-separated, so `;` and `%` /// itself are escaped. Anything else is passed through and then sanitised. fn decode_status_bar_field(raw: &str) -> String { let mut out = String::with_capacity(raw.len()); let mut chars = raw.chars(); while let Some(c) = chars.next() { if c != '%' { out.push(c); continue; } let hi = chars.next(); let lo = chars.next(); match (hi, lo) { (Some(hi), Some(lo)) => { match u8::from_str_radix(&format!("{}{}", hi, lo), 16) { Ok(byte) => out.push(byte as char), // not a valid escape: keep it literal rather than losing text Err(_) => { out.push('%'); out.push(hi); out.push(lo); }, } }, _ => out.push('%'), } } out } fn encode_status_bar_field(raw: &str) -> String { raw.chars() .flat_map(|c| match c { '%' => "%25".chars().collect::>(), ';' => "%3B".chars().collect::>(), ':' => "%3A".chars().collect::>(), other => vec![other], }) .collect() } /// Parse the `buttons` verb's params: each is `: