Skip to main content

zellij_utils/
data.rs

1use crate::home::default_layout_dir;
2use crate::input::actions::{Action, RunCommandAction};
3use crate::input::config::{ConversionError, KdlError};
4use crate::input::keybinds::Keybinds;
5use crate::input::layout::{
6    Layout, PercentOrFixed, Run, RunPlugin, RunPluginLocation, RunPluginOrAlias,
7};
8pub use crate::input::options::PaneFrameStyle;
9use crate::pane_size::PaneGeom;
10use crate::position::Position;
11use crate::shared::{colors as default_colors, eightbit_to_rgb};
12use clap::ValueEnum;
13use serde::{Deserialize, Serialize};
14use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
15use std::fmt;
16use std::fs::Metadata;
17use std::hash::{Hash, Hasher};
18use std::net::IpAddr;
19use std::path::{Path, PathBuf};
20use std::str::{self, FromStr};
21use std::time::Duration;
22use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString};
23use unicode_width::UnicodeWidthChar;
24
25#[cfg(not(target_family = "wasm"))]
26use crate::vendored::termwiz::{
27    input::KittyKeyboardFlags,
28    input::{KeyCode, KeyCodeEncodeModes, KeyboardEncoding, Modifiers},
29};
30
31pub type ClientId = u16; // TODO: merge with crate type?
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34pub enum UnblockCondition {
35    /// Unblock only when exit status is 0 (success)
36    OnExitSuccess,
37    /// Unblock only when exit status is non-zero (failure)
38    OnExitFailure,
39    /// Unblock on any exit (success or failure)
40    OnAnyExit,
41}
42
43impl UnblockCondition {
44    /// Check if the condition is met for the given exit status
45    pub fn is_met(&self, exit_status: i32) -> bool {
46        match self {
47            UnblockCondition::OnExitSuccess => exit_status == 0,
48            UnblockCondition::OnExitFailure => exit_status != 0,
49            UnblockCondition::OnAnyExit => true,
50        }
51    }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub enum CommandOrPlugin {
56    Command(RunCommandAction),
57    Plugin(RunPluginOrAlias),
58    File(FileToOpen), // open file in configured editor
59}
60
61impl CommandOrPlugin {
62    pub fn new_command(command: Vec<String>) -> Self {
63        CommandOrPlugin::Command(RunCommandAction::new(command))
64    }
65}
66
67pub fn client_id_to_colors(
68    client_id: ClientId,
69    colors: MultiplayerColors,
70) -> Option<(PaletteColor, PaletteColor)> {
71    // (primary color, secondary color)
72    let black = PaletteColor::EightBit(default_colors::BLACK);
73    match client_id {
74        1 => Some((colors.player_1, black)),
75        2 => Some((colors.player_2, black)),
76        3 => Some((colors.player_3, black)),
77        4 => Some((colors.player_4, black)),
78        5 => Some((colors.player_5, black)),
79        6 => Some((colors.player_6, black)),
80        7 => Some((colors.player_7, black)),
81        8 => Some((colors.player_8, black)),
82        9 => Some((colors.player_9, black)),
83        10 => Some((colors.player_10, black)),
84        _ => None,
85    }
86}
87
88pub fn single_client_color(colors: Palette) -> (PaletteColor, PaletteColor) {
89    (colors.green, colors.black)
90}
91
92impl FromStr for KeyWithModifier {
93    type Err = Box<dyn std::error::Error>;
94    fn from_str(key_str: &str) -> Result<Self, Self::Err> {
95        let mut key_string_parts: Vec<&str> = key_str.split_ascii_whitespace().collect();
96        let bare_key: BareKey = BareKey::from_str(key_string_parts.pop().ok_or("empty key")?)?;
97        let mut key_modifiers: BTreeSet<KeyModifier> = BTreeSet::new();
98        for stringified_modifier in key_string_parts {
99            key_modifiers.insert(KeyModifier::from_str(stringified_modifier)?);
100        }
101        Ok(KeyWithModifier {
102            bare_key,
103            key_modifiers,
104        })
105    }
106}
107
108#[derive(Debug, Clone, Eq, Serialize, Deserialize, PartialOrd, Ord)]
109pub struct KeyWithModifier {
110    pub bare_key: BareKey,
111    pub key_modifiers: BTreeSet<KeyModifier>,
112}
113
114impl PartialEq for KeyWithModifier {
115    fn eq(&self, other: &Self) -> bool {
116        match (self.bare_key, other.bare_key) {
117            (BareKey::Char(self_char), BareKey::Char(other_char))
118                if self_char.to_ascii_lowercase() == other_char.to_ascii_lowercase() =>
119            {
120                let mut self_cloned = self.clone();
121                let mut other_cloned = other.clone();
122                if self_char.is_ascii_uppercase() {
123                    self_cloned.bare_key = BareKey::Char(self_char.to_ascii_lowercase());
124                    self_cloned.key_modifiers.insert(KeyModifier::Shift);
125                }
126                if other_char.is_ascii_uppercase() {
127                    other_cloned.bare_key = BareKey::Char(self_char.to_ascii_lowercase());
128                    other_cloned.key_modifiers.insert(KeyModifier::Shift);
129                }
130                self_cloned.bare_key == other_cloned.bare_key
131                    && self_cloned.key_modifiers == other_cloned.key_modifiers
132            },
133            _ => self.bare_key == other.bare_key && self.key_modifiers == other.key_modifiers,
134        }
135    }
136}
137
138impl Hash for KeyWithModifier {
139    fn hash<H: Hasher>(&self, state: &mut H) {
140        match self.bare_key {
141            BareKey::Char(character) if character.is_ascii_uppercase() => {
142                let mut to_hash = self.clone();
143                to_hash.bare_key = BareKey::Char(character.to_ascii_lowercase());
144                to_hash.key_modifiers.insert(KeyModifier::Shift);
145                to_hash.bare_key.hash(state);
146                to_hash.key_modifiers.hash(state);
147            },
148            _ => {
149                self.bare_key.hash(state);
150                self.key_modifiers.hash(state);
151            },
152        }
153    }
154}
155
156impl fmt::Display for KeyWithModifier {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        if self.key_modifiers.is_empty() {
159            write!(f, "{}", self.bare_key)
160        } else {
161            write!(
162                f,
163                "{} {}",
164                self.key_modifiers
165                    .iter()
166                    .map(|m| m.to_string())
167                    .collect::<Vec<_>>()
168                    .join(" "),
169                self.bare_key
170            )
171        }
172    }
173}
174
175#[cfg(not(target_family = "wasm"))]
176impl Into<Modifiers> for &KeyModifier {
177    fn into(self) -> Modifiers {
178        match self {
179            KeyModifier::Shift => Modifiers::SHIFT,
180            KeyModifier::Alt => Modifiers::ALT,
181            KeyModifier::Ctrl => Modifiers::CTRL,
182            KeyModifier::Super => Modifiers::SUPER,
183        }
184    }
185}
186
187#[derive(Eq, Clone, Copy, Debug, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
188pub enum BareKey {
189    PageDown,
190    PageUp,
191    Left,
192    Down,
193    Up,
194    Right,
195    Home,
196    End,
197    Backspace,
198    Delete,
199    Insert,
200    F(u8),
201    Char(char),
202    Tab,
203    Esc,
204    Enter,
205    CapsLock,
206    ScrollLock,
207    NumLock,
208    PrintScreen,
209    Pause,
210    Menu,
211}
212
213impl fmt::Display for BareKey {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        match self {
216            BareKey::PageDown => write!(f, "PgDn"),
217            BareKey::PageUp => write!(f, "PgUp"),
218            BareKey::Left => write!(f, "←"),
219            BareKey::Down => write!(f, "↓"),
220            BareKey::Up => write!(f, "↑"),
221            BareKey::Right => write!(f, "→"),
222            BareKey::Home => write!(f, "HOME"),
223            BareKey::End => write!(f, "END"),
224            BareKey::Backspace => write!(f, "BACKSPACE"),
225            BareKey::Delete => write!(f, "DEL"),
226            BareKey::Insert => write!(f, "INS"),
227            BareKey::F(index) => write!(f, "F{}", index),
228            BareKey::Char(' ') => write!(f, "SPACE"),
229            BareKey::Char(character) => write!(f, "{}", character),
230            BareKey::Tab => write!(f, "TAB"),
231            BareKey::Esc => write!(f, "ESC"),
232            BareKey::Enter => write!(f, "ENTER"),
233            BareKey::CapsLock => write!(f, "CAPSlOCK"),
234            BareKey::ScrollLock => write!(f, "SCROLLlOCK"),
235            BareKey::NumLock => write!(f, "NUMLOCK"),
236            BareKey::PrintScreen => write!(f, "PRINTSCREEN"),
237            BareKey::Pause => write!(f, "PAUSE"),
238            BareKey::Menu => write!(f, "MENU"),
239        }
240    }
241}
242
243impl FromStr for BareKey {
244    type Err = Box<dyn std::error::Error>;
245    fn from_str(key_str: &str) -> Result<Self, Self::Err> {
246        match key_str.to_ascii_lowercase().as_str() {
247            "pagedown" => Ok(BareKey::PageDown),
248            "pageup" => Ok(BareKey::PageUp),
249            "left" => Ok(BareKey::Left),
250            "down" => Ok(BareKey::Down),
251            "up" => Ok(BareKey::Up),
252            "right" => Ok(BareKey::Right),
253            "home" => Ok(BareKey::Home),
254            "end" => Ok(BareKey::End),
255            "backspace" => Ok(BareKey::Backspace),
256            "delete" => Ok(BareKey::Delete),
257            "del" => Ok(BareKey::Delete),
258            "insert" => Ok(BareKey::Insert),
259            "f1" => Ok(BareKey::F(1)),
260            "f2" => Ok(BareKey::F(2)),
261            "f3" => Ok(BareKey::F(3)),
262            "f4" => Ok(BareKey::F(4)),
263            "f5" => Ok(BareKey::F(5)),
264            "f6" => Ok(BareKey::F(6)),
265            "f7" => Ok(BareKey::F(7)),
266            "f8" => Ok(BareKey::F(8)),
267            "f9" => Ok(BareKey::F(9)),
268            "f10" => Ok(BareKey::F(10)),
269            "f11" => Ok(BareKey::F(11)),
270            "f12" => Ok(BareKey::F(12)),
271            "tab" => Ok(BareKey::Tab),
272            "esc" => Ok(BareKey::Esc),
273            "enter" => Ok(BareKey::Enter),
274            "capslock" => Ok(BareKey::CapsLock),
275            "scrolllock" => Ok(BareKey::ScrollLock),
276            "numlock" => Ok(BareKey::NumLock),
277            "printscreen" => Ok(BareKey::PrintScreen),
278            "pause" => Ok(BareKey::Pause),
279            "menu" => Ok(BareKey::Menu),
280            "space" => Ok(BareKey::Char(' ')),
281            _ => {
282                if key_str.chars().count() == 1 {
283                    if let Some(character) = key_str.chars().next() {
284                        return Ok(BareKey::Char(character));
285                    }
286                }
287                Err("unsupported key".into())
288            },
289        }
290    }
291}
292
293#[derive(
294    Eq, Clone, Copy, Debug, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, Display,
295)]
296pub enum KeyModifier {
297    Ctrl,
298    Alt,
299    Shift,
300    Super,
301}
302
303impl FromStr for KeyModifier {
304    type Err = Box<dyn std::error::Error>;
305    fn from_str(key_str: &str) -> Result<Self, Self::Err> {
306        match key_str.to_ascii_lowercase().as_str() {
307            "shift" => Ok(KeyModifier::Shift),
308            "alt" => Ok(KeyModifier::Alt),
309            "ctrl" => Ok(KeyModifier::Ctrl),
310            "super" => Ok(KeyModifier::Super),
311            _ => Err("unsupported modifier".into()),
312        }
313    }
314}
315
316impl BareKey {
317    pub fn from_bytes_with_u(bytes: &[u8]) -> Option<Self> {
318        match str::from_utf8(bytes) {
319            Ok("27") => Some(BareKey::Esc),
320            Ok("13") => Some(BareKey::Enter),
321            Ok("9") => Some(BareKey::Tab),
322            Ok("127") => Some(BareKey::Backspace),
323            Ok("57358") => Some(BareKey::CapsLock),
324            Ok("57359") => Some(BareKey::ScrollLock),
325            Ok("57360") => Some(BareKey::NumLock),
326            Ok("57361") => Some(BareKey::PrintScreen),
327            Ok("57362") => Some(BareKey::Pause),
328            Ok("57363") => Some(BareKey::Menu),
329            Ok("57399") => Some(BareKey::Char('0')),
330            Ok("57400") => Some(BareKey::Char('1')),
331            Ok("57401") => Some(BareKey::Char('2')),
332            Ok("57402") => Some(BareKey::Char('3')),
333            Ok("57403") => Some(BareKey::Char('4')),
334            Ok("57404") => Some(BareKey::Char('5')),
335            Ok("57405") => Some(BareKey::Char('6')),
336            Ok("57406") => Some(BareKey::Char('7')),
337            Ok("57407") => Some(BareKey::Char('8')),
338            Ok("57408") => Some(BareKey::Char('9')),
339            Ok("57409") => Some(BareKey::Char('.')),
340            Ok("57410") => Some(BareKey::Char('/')),
341            Ok("57411") => Some(BareKey::Char('*')),
342            Ok("57412") => Some(BareKey::Char('-')),
343            Ok("57413") => Some(BareKey::Char('+')),
344            Ok("57414") => Some(BareKey::Enter),
345            Ok("57415") => Some(BareKey::Char('=')),
346            Ok("57417") => Some(BareKey::Left),
347            Ok("57418") => Some(BareKey::Right),
348            Ok("57419") => Some(BareKey::Up),
349            Ok("57420") => Some(BareKey::Down),
350            Ok("57421") => Some(BareKey::PageUp),
351            Ok("57422") => Some(BareKey::PageDown),
352            Ok("57423") => Some(BareKey::Home),
353            Ok("57424") => Some(BareKey::End),
354            Ok("57425") => Some(BareKey::Insert),
355            Ok("57426") => Some(BareKey::Delete),
356            Ok(num) => u32::from_str_radix(num, 10)
357                .ok()
358                .and_then(char::from_u32)
359                .map(BareKey::Char),
360            _ => None,
361        }
362    }
363    pub fn from_bytes_with_tilde(bytes: &[u8]) -> Option<Self> {
364        match str::from_utf8(bytes) {
365            Ok("2") => Some(BareKey::Insert),
366            Ok("3") => Some(BareKey::Delete),
367            Ok("5") => Some(BareKey::PageUp),
368            Ok("6") => Some(BareKey::PageDown),
369            Ok("7") => Some(BareKey::Home),
370            Ok("8") => Some(BareKey::End),
371            Ok("11") => Some(BareKey::F(1)),
372            Ok("12") => Some(BareKey::F(2)),
373            Ok("13") => Some(BareKey::F(3)),
374            Ok("14") => Some(BareKey::F(4)),
375            Ok("15") => Some(BareKey::F(5)),
376            Ok("17") => Some(BareKey::F(6)),
377            Ok("18") => Some(BareKey::F(7)),
378            Ok("19") => Some(BareKey::F(8)),
379            Ok("20") => Some(BareKey::F(9)),
380            Ok("21") => Some(BareKey::F(10)),
381            Ok("23") => Some(BareKey::F(11)),
382            Ok("24") => Some(BareKey::F(12)),
383            _ => None,
384        }
385    }
386    pub fn from_bytes_with_no_ending_byte(bytes: &[u8]) -> Option<Self> {
387        match str::from_utf8(bytes) {
388            Ok("1D") | Ok("D") => Some(BareKey::Left),
389            Ok("1C") | Ok("C") => Some(BareKey::Right),
390            Ok("1A") | Ok("A") => Some(BareKey::Up),
391            Ok("1B") | Ok("B") => Some(BareKey::Down),
392            Ok("1H") | Ok("H") => Some(BareKey::Home),
393            Ok("1F") | Ok("F") => Some(BareKey::End),
394            Ok("1P") | Ok("P") => Some(BareKey::F(1)),
395            Ok("1Q") | Ok("Q") => Some(BareKey::F(2)),
396            Ok("1S") | Ok("S") => Some(BareKey::F(4)),
397            _ => None,
398        }
399    }
400}
401
402bitflags::bitflags! {
403    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
404    struct ModifierFlags: u8 {
405        const SHIFT   = 0b0000_0001;
406        const ALT     = 0b0000_0010;
407        const CONTROL = 0b0000_0100;
408        const SUPER   = 0b0000_1000;
409        // we don't actually use the below, left here for completeness in case we want to add them
410        // later
411        const HYPER = 0b0001_0000;
412        const META = 0b0010_0000;
413        const CAPS_LOCK = 0b0100_0000;
414        const NUM_LOCK = 0b1000_0000;
415    }
416}
417
418impl KeyModifier {
419    pub fn from_bytes(bytes: &[u8]) -> BTreeSet<KeyModifier> {
420        let modifier_flags = str::from_utf8(bytes)
421            .ok() // convert to string: (eg. "16")
422            .and_then(|s| u8::from_str_radix(&s, 10).ok()) // convert to u8: (eg. 16)
423            .map(|s| s.saturating_sub(1)) // subtract 1: (eg. 15)
424            .and_then(|b| ModifierFlags::from_bits(b)); // bitflags: (0b0000_1111: Shift, Alt, Control, Super)
425        let mut key_modifiers = BTreeSet::new();
426        if let Some(modifier_flags) = modifier_flags {
427            for name in modifier_flags.iter() {
428                match name {
429                    ModifierFlags::SHIFT => key_modifiers.insert(KeyModifier::Shift),
430                    ModifierFlags::ALT => key_modifiers.insert(KeyModifier::Alt),
431                    ModifierFlags::CONTROL => key_modifiers.insert(KeyModifier::Ctrl),
432                    ModifierFlags::SUPER => key_modifiers.insert(KeyModifier::Super),
433                    _ => false,
434                };
435            }
436        }
437        key_modifiers
438    }
439}
440
441impl KeyWithModifier {
442    pub fn new(bare_key: BareKey) -> Self {
443        KeyWithModifier {
444            bare_key,
445            key_modifiers: BTreeSet::new(),
446        }
447    }
448    pub fn new_with_modifiers(bare_key: BareKey, key_modifiers: BTreeSet<KeyModifier>) -> Self {
449        KeyWithModifier {
450            bare_key,
451            key_modifiers,
452        }
453    }
454    pub fn with_shift_modifier(mut self) -> Self {
455        self.key_modifiers.insert(KeyModifier::Shift);
456        self
457    }
458    pub fn with_alt_modifier(mut self) -> Self {
459        self.key_modifiers.insert(KeyModifier::Alt);
460        self
461    }
462    pub fn with_ctrl_modifier(mut self) -> Self {
463        self.key_modifiers.insert(KeyModifier::Ctrl);
464        self
465    }
466    pub fn with_super_modifier(mut self) -> Self {
467        self.key_modifiers.insert(KeyModifier::Super);
468        self
469    }
470    pub fn from_bytes_with_u(number_bytes: &[u8], modifier_bytes: &[u8]) -> Option<Self> {
471        // CSI number ; modifiers u
472        let bare_key = BareKey::from_bytes_with_u(number_bytes);
473        match bare_key {
474            Some(bare_key) => {
475                let key_modifiers = KeyModifier::from_bytes(modifier_bytes);
476                Some(KeyWithModifier {
477                    bare_key,
478                    key_modifiers,
479                })
480            },
481            _ => None,
482        }
483    }
484    pub fn from_bytes_with_tilde(number_bytes: &[u8], modifier_bytes: &[u8]) -> Option<Self> {
485        // CSI number ; modifiers ~
486        let bare_key = BareKey::from_bytes_with_tilde(number_bytes);
487        match bare_key {
488            Some(bare_key) => {
489                let key_modifiers = KeyModifier::from_bytes(modifier_bytes);
490                Some(KeyWithModifier {
491                    bare_key,
492                    key_modifiers,
493                })
494            },
495            _ => None,
496        }
497    }
498    pub fn from_bytes_with_no_ending_byte(
499        number_bytes: &[u8],
500        modifier_bytes: &[u8],
501    ) -> Option<Self> {
502        // CSI 1; modifiers [ABCDEFHPQS]
503        let bare_key = BareKey::from_bytes_with_no_ending_byte(number_bytes);
504        match bare_key {
505            Some(bare_key) => {
506                let key_modifiers = KeyModifier::from_bytes(modifier_bytes);
507                Some(KeyWithModifier {
508                    bare_key,
509                    key_modifiers,
510                })
511            },
512            _ => None,
513        }
514    }
515    pub fn strip_common_modifiers(&self, common_modifiers: &Vec<KeyModifier>) -> Self {
516        let common_modifiers: BTreeSet<&KeyModifier> = common_modifiers.into_iter().collect();
517        KeyWithModifier {
518            bare_key: self.bare_key.clone(),
519            key_modifiers: self
520                .key_modifiers
521                .iter()
522                .filter(|m| !common_modifiers.contains(m))
523                .cloned()
524                .collect(),
525        }
526    }
527    pub fn is_key_without_modifier(&self, key: BareKey) -> bool {
528        self.bare_key == key && self.key_modifiers.is_empty()
529    }
530    pub fn is_key_with_ctrl_modifier(&self, key: BareKey) -> bool {
531        self.bare_key == key && self.key_modifiers.contains(&KeyModifier::Ctrl)
532    }
533    pub fn is_key_with_alt_modifier(&self, key: BareKey) -> bool {
534        self.bare_key == key && self.key_modifiers.contains(&KeyModifier::Alt)
535    }
536    pub fn is_key_with_shift_modifier(&self, key: BareKey) -> bool {
537        self.bare_key == key && self.key_modifiers.contains(&KeyModifier::Shift)
538    }
539    pub fn is_key_with_super_modifier(&self, key: BareKey) -> bool {
540        self.bare_key == key && self.key_modifiers.contains(&KeyModifier::Super)
541    }
542    pub fn is_cancel_key(&self) -> bool {
543        // self.bare_key == BareKey::Esc || self.is_key_with_ctrl_modifier(BareKey::Char('c'))
544        self.bare_key == BareKey::Esc
545    }
546    #[cfg(not(target_family = "wasm"))]
547    pub fn to_termwiz_modifiers(&self) -> Modifiers {
548        let mut modifiers = Modifiers::empty();
549        for modifier in &self.key_modifiers {
550            modifiers.set(modifier.into(), true);
551        }
552        modifiers
553    }
554    #[cfg(not(target_family = "wasm"))]
555    pub fn to_termwiz_keycode(&self) -> KeyCode {
556        match self.bare_key {
557            BareKey::PageDown => KeyCode::PageDown,
558            BareKey::PageUp => KeyCode::PageUp,
559            BareKey::Left => KeyCode::LeftArrow,
560            BareKey::Down => KeyCode::DownArrow,
561            BareKey::Up => KeyCode::UpArrow,
562            BareKey::Right => KeyCode::RightArrow,
563            BareKey::Home => KeyCode::Home,
564            BareKey::End => KeyCode::End,
565            BareKey::Backspace => KeyCode::Backspace,
566            BareKey::Delete => KeyCode::Delete,
567            BareKey::Insert => KeyCode::Insert,
568            BareKey::F(index) => KeyCode::Function(index),
569            BareKey::Char(character) => KeyCode::Char(character),
570            BareKey::Tab => KeyCode::Tab,
571            BareKey::Esc => KeyCode::Escape,
572            BareKey::Enter => KeyCode::Enter,
573            BareKey::CapsLock => KeyCode::CapsLock,
574            BareKey::ScrollLock => KeyCode::ScrollLock,
575            BareKey::NumLock => KeyCode::NumLock,
576            BareKey::PrintScreen => KeyCode::PrintScreen,
577            BareKey::Pause => KeyCode::Pause,
578            BareKey::Menu => KeyCode::Menu,
579        }
580    }
581    #[cfg(not(target_family = "wasm"))]
582    pub fn serialize_non_kitty(&self) -> Option<String> {
583        let modifiers = self.to_termwiz_modifiers();
584        let key_code_encode_modes = KeyCodeEncodeModes {
585            encoding: KeyboardEncoding::Xterm,
586            // all these flags are false because they have been dealt with before this
587            // serialization
588            application_cursor_keys: false,
589            newline_mode: false,
590            modify_other_keys: None,
591        };
592        self.to_termwiz_keycode()
593            .encode(modifiers, key_code_encode_modes, true)
594            .ok()
595    }
596    #[cfg(not(target_family = "wasm"))]
597    pub fn serialize_kitty(&self) -> Option<String> {
598        let modifiers = self.to_termwiz_modifiers();
599        let key_code_encode_modes = KeyCodeEncodeModes {
600            encoding: KeyboardEncoding::Kitty(KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES),
601            // all these flags are false because they have been dealt with before this
602            // serialization
603            application_cursor_keys: false,
604            newline_mode: false,
605            modify_other_keys: None,
606        };
607        self.to_termwiz_keycode()
608            .encode(modifiers, key_code_encode_modes, true)
609            .ok()
610    }
611    pub fn has_no_modifiers(&self) -> bool {
612        self.key_modifiers.is_empty()
613    }
614    pub fn has_modifiers(&self, modifiers: &[KeyModifier]) -> bool {
615        for modifier in modifiers {
616            if !self.key_modifiers.contains(modifier) {
617                return false;
618            }
619        }
620        true
621    }
622    pub fn has_only_modifiers(&self, modifiers: &[KeyModifier]) -> bool {
623        for modifier in modifiers {
624            if !self.key_modifiers.contains(modifier) {
625                return false;
626            }
627        }
628        if self.key_modifiers.len() != modifiers.len() {
629            return false;
630        }
631        true
632    }
633}
634
635#[derive(Eq, Clone, Copy, Debug, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
636pub enum Direction {
637    Left,
638    Right,
639    Up,
640    Down,
641}
642
643impl Default for Direction {
644    fn default() -> Self {
645        Direction::Left
646    }
647}
648
649impl Direction {
650    pub fn invert(&self) -> Direction {
651        match *self {
652            Direction::Left => Direction::Right,
653            Direction::Down => Direction::Up,
654            Direction::Up => Direction::Down,
655            Direction::Right => Direction::Left,
656        }
657    }
658
659    pub fn is_horizontal(&self) -> bool {
660        matches!(self, Direction::Left | Direction::Right)
661    }
662
663    pub fn is_vertical(&self) -> bool {
664        matches!(self, Direction::Down | Direction::Up)
665    }
666}
667
668impl fmt::Display for Direction {
669    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670        match self {
671            Direction::Left => write!(f, "←"),
672            Direction::Right => write!(f, "→"),
673            Direction::Up => write!(f, "↑"),
674            Direction::Down => write!(f, "↓"),
675        }
676    }
677}
678
679impl FromStr for Direction {
680    type Err = String;
681    fn from_str(s: &str) -> Result<Self, Self::Err> {
682        match s {
683            "Left" | "left" => Ok(Direction::Left),
684            "Right" | "right" => Ok(Direction::Right),
685            "Up" | "up" => Ok(Direction::Up),
686            "Down" | "down" => Ok(Direction::Down),
687            _ => Err(format!(
688                "Failed to parse Direction. Unknown Direction: {}",
689                s
690            )),
691        }
692    }
693}
694
695/// Resize operation to perform.
696#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Deserialize, Serialize)]
697pub enum Resize {
698    Increase,
699    Decrease,
700}
701
702impl Default for Resize {
703    fn default() -> Self {
704        Resize::Increase
705    }
706}
707
708impl Resize {
709    pub fn invert(&self) -> Self {
710        match self {
711            Resize::Increase => Resize::Decrease,
712            Resize::Decrease => Resize::Increase,
713        }
714    }
715}
716
717impl fmt::Display for Resize {
718    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
719        match self {
720            Resize::Increase => write!(f, "+"),
721            Resize::Decrease => write!(f, "-"),
722        }
723    }
724}
725
726impl FromStr for Resize {
727    type Err = String;
728    fn from_str(s: &str) -> Result<Self, Self::Err> {
729        match s {
730            "Increase" | "increase" | "+" => Ok(Resize::Increase),
731            "Decrease" | "decrease" | "-" => Ok(Resize::Decrease),
732            _ => Err(format!(
733                "failed to parse resize type. Unknown specifier '{}'",
734                s
735            )),
736        }
737    }
738}
739
740/// Container type that fully describes resize operations.
741///
742/// This is best thought of as follows:
743///
744/// - `resize` commands how the total *area* of the pane will change as part of this resize
745///   operation.
746/// - `direction` has two meanings:
747///     - `None` means to resize all borders equally
748///     - Anything else means to move the named border to achieve the change in area
749#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Deserialize, Serialize)]
750pub struct ResizeStrategy {
751    /// Whether to increase or resize total area
752    pub resize: Resize,
753    /// With which border, if any, to change area
754    pub direction: Option<Direction>,
755    /// If set to true (default), increasing resizes towards a viewport border will be inverted.
756    /// I.e. a scenario like this ("increase right"):
757    ///
758    /// ```text
759    /// +---+---+
760    /// |   | X |->
761    /// +---+---+
762    /// ```
763    ///
764    /// turns into this ("decrease left"):
765    ///
766    /// ```text
767    /// +---+---+
768    /// |   |-> |
769    /// +---+---+
770    /// ```
771    pub invert_on_boundaries: bool,
772}
773
774impl From<Direction> for ResizeStrategy {
775    fn from(direction: Direction) -> Self {
776        ResizeStrategy::new(Resize::Increase, Some(direction))
777    }
778}
779
780impl From<Resize> for ResizeStrategy {
781    fn from(resize: Resize) -> Self {
782        ResizeStrategy::new(resize, None)
783    }
784}
785
786impl ResizeStrategy {
787    pub fn new(resize: Resize, direction: Option<Direction>) -> Self {
788        ResizeStrategy {
789            resize,
790            direction,
791            invert_on_boundaries: true,
792        }
793    }
794
795    pub fn invert(&self) -> ResizeStrategy {
796        let resize = match self.resize {
797            Resize::Increase => Resize::Decrease,
798            Resize::Decrease => Resize::Increase,
799        };
800        let direction = match self.direction {
801            Some(direction) => Some(direction.invert()),
802            None => None,
803        };
804
805        ResizeStrategy::new(resize, direction)
806    }
807
808    pub fn resize_type(&self) -> Resize {
809        self.resize
810    }
811
812    pub fn direction(&self) -> Option<Direction> {
813        self.direction
814    }
815
816    pub fn direction_horizontal(&self) -> bool {
817        matches!(
818            self.direction,
819            Some(Direction::Left) | Some(Direction::Right)
820        )
821    }
822
823    pub fn direction_vertical(&self) -> bool {
824        matches!(self.direction, Some(Direction::Up) | Some(Direction::Down))
825    }
826
827    pub fn resize_increase(&self) -> bool {
828        self.resize == Resize::Increase
829    }
830
831    pub fn resize_decrease(&self) -> bool {
832        self.resize == Resize::Decrease
833    }
834
835    pub fn move_left_border_left(&self) -> bool {
836        (self.resize == Resize::Increase) && (self.direction == Some(Direction::Left))
837    }
838
839    pub fn move_left_border_right(&self) -> bool {
840        (self.resize == Resize::Decrease) && (self.direction == Some(Direction::Left))
841    }
842
843    pub fn move_lower_border_down(&self) -> bool {
844        (self.resize == Resize::Increase) && (self.direction == Some(Direction::Down))
845    }
846
847    pub fn move_lower_border_up(&self) -> bool {
848        (self.resize == Resize::Decrease) && (self.direction == Some(Direction::Down))
849    }
850
851    pub fn move_upper_border_up(&self) -> bool {
852        (self.resize == Resize::Increase) && (self.direction == Some(Direction::Up))
853    }
854
855    pub fn move_upper_border_down(&self) -> bool {
856        (self.resize == Resize::Decrease) && (self.direction == Some(Direction::Up))
857    }
858
859    pub fn move_right_border_right(&self) -> bool {
860        (self.resize == Resize::Increase) && (self.direction == Some(Direction::Right))
861    }
862
863    pub fn move_right_border_left(&self) -> bool {
864        (self.resize == Resize::Decrease) && (self.direction == Some(Direction::Right))
865    }
866
867    pub fn move_all_borders_out(&self) -> bool {
868        (self.resize == Resize::Increase) && (self.direction == None)
869    }
870
871    pub fn move_all_borders_in(&self) -> bool {
872        (self.resize == Resize::Decrease) && (self.direction == None)
873    }
874}
875
876impl fmt::Display for ResizeStrategy {
877    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
878        let resize = match self.resize {
879            Resize::Increase => "increase",
880            Resize::Decrease => "decrease",
881        };
882        let border = match self.direction {
883            Some(Direction::Left) => "left",
884            Some(Direction::Down) => "bottom",
885            Some(Direction::Up) => "top",
886            Some(Direction::Right) => "right",
887            None => "every",
888        };
889
890        write!(f, "{} size on {} border", resize, border)
891    }
892}
893
894#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
895// FIXME: This should be extended to handle different button clicks (not just
896// left click) and the `ScrollUp` and `ScrollDown` events could probably be
897// merged into a single `Scroll(isize)` event.
898pub enum Mouse {
899    ScrollUp(usize),   // number of lines
900    ScrollDown(usize), // number of lines
901    ScrollLeft(usize),
902    ScrollRight(usize),
903    LeftClick(isize, usize),  // line and column
904    RightClick(isize, usize), // line and column
905    Hold(isize, usize),       // line and column
906    Release(isize, usize),    // line and column
907    Hover(isize, usize),      // line and column
908}
909
910impl Mouse {
911    pub fn position(&self) -> Option<(usize, usize)> {
912        // (line, column)
913        match self {
914            Mouse::LeftClick(line, column) => Some((*line as usize, *column as usize)),
915            Mouse::RightClick(line, column) => Some((*line as usize, *column as usize)),
916            Mouse::Hold(line, column) => Some((*line as usize, *column as usize)),
917            Mouse::Release(line, column) => Some((*line as usize, *column as usize)),
918            Mouse::Hover(line, column) => Some((*line as usize, *column as usize)),
919            _ => None,
920        }
921    }
922}
923
924#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
925pub struct FileMetadata {
926    pub is_dir: bool,
927    pub is_file: bool,
928    pub is_symlink: bool,
929    pub len: u64,
930}
931
932impl From<Metadata> for FileMetadata {
933    fn from(metadata: Metadata) -> Self {
934        FileMetadata {
935            is_dir: metadata.is_dir(),
936            is_file: metadata.is_file(),
937            is_symlink: metadata.is_symlink(),
938            len: metadata.len(),
939        }
940    }
941}
942
943#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
944pub struct StyledText {
945    pub text: String,
946    pub indices: Vec<Vec<usize>>,
947}
948
949/// These events can be subscribed to with subscribe method exported by `zellij-tile`.
950/// Once subscribed to, they will trigger the `update` method of the `ZellijPlugin` trait.
951#[derive(Debug, Clone, PartialEq, EnumDiscriminants, Display, Serialize, Deserialize)]
952#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))]
953#[strum_discriminants(name(EventType))]
954#[non_exhaustive]
955pub enum Event {
956    ModeUpdate(ModeInfo),
957    TabUpdate(Vec<TabInfo>),
958    PaneUpdate(PaneManifest),
959    /// A key was pressed while the user is focused on this plugin's pane
960    Key(KeyWithModifier),
961    /// A mouse event happened while the user is focused on this plugin's pane
962    Mouse(Mouse),
963    /// A timer expired set by the `set_timeout` method exported by `zellij-tile`.
964    Timer(f64),
965    /// Text was copied to the clipboard anywhere in the app
966    CopyToClipboard(CopyDestination),
967    /// Failed to copy text to clipboard anywhere in the app
968    SystemClipboardFailure,
969    /// Input was received anywhere in the app
970    InputReceived,
971    /// This plugin became visible or invisible
972    Visible(bool),
973    /// A message from one of the plugin's workers
974    CustomMessage(
975        String, // message
976        String, // payload
977    ),
978    /// A file was created somewhere in the Zellij CWD folder
979    FileSystemCreate(Vec<(PathBuf, Option<FileMetadata>)>),
980    /// A file was accessed somewhere in the Zellij CWD folder
981    FileSystemRead(Vec<(PathBuf, Option<FileMetadata>)>),
982    /// A file was modified somewhere in the Zellij CWD folder
983    FileSystemUpdate(Vec<(PathBuf, Option<FileMetadata>)>),
984    /// A file was deleted somewhere in the Zellij CWD folder
985    FileSystemDelete(Vec<(PathBuf, Option<FileMetadata>)>),
986    /// A Result of plugin permission request
987    PermissionRequestResult(PermissionStatus),
988    SessionUpdate(
989        Vec<SessionInfo>,
990        Vec<(String, Duration)>, // resurrectable sessions
991    ),
992    RunCommandResult(Option<i32>, Vec<u8>, Vec<u8>, BTreeMap<String, String>), // exit_code, STDOUT, STDERR,
993    // context
994    WebRequestResult(
995        u16,
996        BTreeMap<String, String>,
997        Vec<u8>,
998        BTreeMap<String, String>,
999    ), // status,
1000    // headers,
1001    // body,
1002    // context
1003    CommandPaneOpened(u32, Context), // u32 - terminal_pane_id
1004    CommandPaneExited(u32, Option<i32>, Context), // u32 - terminal_pane_id, Option<i32> -
1005    // exit_code
1006    PaneClosed(PaneId),
1007    EditPaneOpened(u32, Context),              // u32 - terminal_pane_id
1008    EditPaneExited(u32, Option<i32>, Context), // u32 - terminal_pane_id, Option<i32> - exit code
1009    CommandPaneReRun(u32, Context),            // u32 - terminal_pane_id, Option<i32> -
1010    FailedToWriteConfigToDisk(Option<String>), // String -> the file path we failed to write
1011    ListClients(Vec<ClientInfo>),
1012    HostFolderChanged(PathBuf),               // PathBuf -> new host folder
1013    FailedToChangeHostFolder(Option<String>), // String -> the error we got when changing
1014    PastedText(String),
1015    ConfigWasWrittenToDisk,
1016    WebServerStatus(WebServerStatus),
1017    FailedToStartWebServer(String),
1018    BeforeClose,
1019    InterceptedKeyPress(KeyWithModifier),
1020    /// An action was performed by the user (requires InterceptInput permission)
1021    UserAction(Action, ClientId, Option<u32>, Option<ClientId>), // Action, client_id, terminal_id, cli_client_id
1022    PaneRenderReport(HashMap<PaneId, PaneContents>),
1023    ActionComplete(Action, Option<PaneId>, BTreeMap<String, String>), // Action, pane_id, context
1024    CwdChanged(PaneId, PathBuf, Vec<ClientId>), // pane_id, cwd, focused_client_ids
1025    CommandChanged(PaneId, Vec<String>, bool, Vec<ClientId>), // pane_id, command, is_foreground, focused_client_ids
1026    AvailableLayoutInfo(Vec<LayoutInfo>, Vec<LayoutWithError>),
1027    PluginConfigurationChanged(BTreeMap<String, String>),
1028    HighlightClicked {
1029        pane_id: PaneId,
1030        pattern: String,
1031        matched_string: String,
1032        context: BTreeMap<String, String>,
1033    },
1034    /// Initial keybindings sent once on plugin load and on reconfiguration.
1035    /// Plugins that subscribe to this event signal they cache keybindings
1036    /// and can handle lightweight ModeUpdate events without keybindings.
1037    InitialKeybinds(KeybindsVec),
1038    /// The host terminal indicated its color palette theme mode (CSI 2031 / DSR 997).
1039    HostTerminalThemeChanged(HostTerminalThemeMode),
1040    SoftKeyboardVisibilityChanged(bool),
1041    HintText(BTreeMap<usize, StyledText>),
1042    ActivePaneScroll(Option<(usize, usize)>),
1043}
1044
1045#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1046pub enum HostTerminalThemeMode {
1047    Dark,
1048    Light,
1049}
1050
1051#[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants, Display, Serialize, Deserialize)]
1052pub enum WebServerStatus {
1053    Online(String), // String -> base url
1054    Offline,
1055    DifferentVersion(String), // version
1056}
1057
1058#[derive(
1059    Debug,
1060    PartialEq,
1061    Eq,
1062    Hash,
1063    Copy,
1064    Clone,
1065    EnumDiscriminants,
1066    Display,
1067    Serialize,
1068    Deserialize,
1069    PartialOrd,
1070    Ord,
1071)]
1072#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize, Display, PartialOrd, Ord))]
1073#[strum_discriminants(name(PermissionType))]
1074#[non_exhaustive]
1075pub enum Permission {
1076    ReadApplicationState,
1077    ChangeApplicationState,
1078    OpenFiles,
1079    RunCommands,
1080    OpenTerminalsOrPlugins,
1081    WriteToStdin,
1082    WebAccess,
1083    ReadCliPipes,
1084    MessageAndLaunchOtherPlugins,
1085    Reconfigure,
1086    FullHdAccess,
1087    StartWebServer,
1088    InterceptInput,
1089    ReadPaneContents,
1090    RunActionsAsUser,
1091    WriteToClipboard,
1092    ReadSessionEnvironmentVariables,
1093}
1094
1095impl PermissionType {
1096    pub fn display_name(&self) -> String {
1097        match self {
1098            PermissionType::ReadApplicationState => {
1099                "Access Zellij state (Panes, Tabs and UI)".to_owned()
1100            },
1101            PermissionType::ChangeApplicationState => {
1102                "Change Zellij state (Panes, Tabs and UI) and run commands".to_owned()
1103            },
1104            PermissionType::OpenFiles => "Open files (eg. for editing)".to_owned(),
1105            PermissionType::RunCommands => "Run commands".to_owned(),
1106            PermissionType::OpenTerminalsOrPlugins => "Start new terminals and plugins".to_owned(),
1107            PermissionType::WriteToStdin => "Write to standard input (STDIN)".to_owned(),
1108            PermissionType::WebAccess => "Make web requests".to_owned(),
1109            PermissionType::ReadCliPipes => "Control command line pipes and output".to_owned(),
1110            PermissionType::MessageAndLaunchOtherPlugins => {
1111                "Send messages to and launch other plugins".to_owned()
1112            },
1113            PermissionType::Reconfigure => "Change Zellij runtime configuration".to_owned(),
1114            PermissionType::FullHdAccess => "Full access to the hard-drive".to_owned(),
1115            PermissionType::StartWebServer => {
1116                "Start a local web server to serve Zellij sessions".to_owned()
1117            },
1118            PermissionType::InterceptInput => "Intercept Input (keyboard & mouse)".to_owned(),
1119            PermissionType::ReadPaneContents => {
1120                "Read pane contents (viewport and selection)".to_owned()
1121            },
1122            PermissionType::RunActionsAsUser => "Execute actions as the user".to_owned(),
1123            PermissionType::WriteToClipboard => "Write to clipboard".to_owned(),
1124            PermissionType::ReadSessionEnvironmentVariables => {
1125                "Read environment variables present upon session creation".to_owned()
1126            },
1127        }
1128    }
1129}
1130
1131#[derive(Debug, Clone)]
1132pub struct PluginPermission {
1133    pub name: String,
1134    pub permissions: Vec<PermissionType>,
1135}
1136
1137impl PluginPermission {
1138    pub fn new(name: String, permissions: Vec<PermissionType>) -> Self {
1139        PluginPermission { name, permissions }
1140    }
1141}
1142
1143/// Describes the different input modes, which change the way that keystrokes will be interpreted.
1144#[derive(
1145    Debug,
1146    PartialEq,
1147    Eq,
1148    Hash,
1149    Copy,
1150    Clone,
1151    EnumIter,
1152    Serialize,
1153    Deserialize,
1154    ValueEnum,
1155    PartialOrd,
1156    Ord,
1157)]
1158pub enum InputMode {
1159    /// In `Normal` mode, input is always written to the terminal, except for the shortcuts leading
1160    /// to other modes
1161    #[serde(alias = "normal")]
1162    Normal,
1163    /// In `Locked` mode, input is always written to the terminal and all shortcuts are disabled
1164    /// except the one leading back to normal mode
1165    #[serde(alias = "locked")]
1166    Locked,
1167    /// `Resize` mode allows resizing the different existing panes.
1168    #[serde(alias = "resize")]
1169    Resize,
1170    /// `Pane` mode allows creating and closing panes, as well as moving between them.
1171    #[serde(alias = "pane")]
1172    Pane,
1173    /// `Tab` mode allows creating and closing tabs, as well as moving between them.
1174    #[serde(alias = "tab")]
1175    Tab,
1176    /// `Scroll` mode allows scrolling up and down within a pane.
1177    #[serde(alias = "scroll")]
1178    Scroll,
1179    /// `EnterSearch` mode allows for typing in the needle for a search in the scroll buffer of a pane.
1180    #[serde(alias = "entersearch")]
1181    EnterSearch,
1182    /// `Search` mode allows for searching a term in a pane (superset of `Scroll`).
1183    #[serde(alias = "search")]
1184    Search,
1185    /// `RenameTab` mode allows assigning a new name to a tab.
1186    #[serde(alias = "renametab")]
1187    RenameTab,
1188    /// `RenamePane` mode allows assigning a new name to a pane.
1189    #[serde(alias = "renamepane")]
1190    RenamePane,
1191    /// `Session` mode allows detaching sessions
1192    #[serde(alias = "session")]
1193    Session,
1194    /// `Move` mode allows moving the different existing panes within a tab
1195    #[serde(alias = "move")]
1196    Move,
1197    /// `Prompt` mode allows interacting with active prompts.
1198    #[serde(alias = "prompt")]
1199    Prompt,
1200    /// `Tmux` mode allows for basic tmux keybindings functionality
1201    #[serde(alias = "tmux")]
1202    Tmux,
1203}
1204
1205impl Default for InputMode {
1206    fn default() -> InputMode {
1207        InputMode::Normal
1208    }
1209}
1210
1211#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
1212pub enum ThemeHue {
1213    Light,
1214    Dark,
1215}
1216impl Default for ThemeHue {
1217    fn default() -> ThemeHue {
1218        ThemeHue::Dark
1219    }
1220}
1221
1222#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
1223pub enum PaletteColor {
1224    Rgb((u8, u8, u8)),
1225    EightBit(u8),
1226}
1227impl Default for PaletteColor {
1228    fn default() -> PaletteColor {
1229        PaletteColor::EightBit(0)
1230    }
1231}
1232
1233/// Priority layer for plugin-supplied regex highlights.
1234/// Higher-priority layers take visual precedence over lower ones
1235/// when highlights overlap.  Built-in highlights (mouse selection,
1236/// search results) always take precedence over all plugin layers.
1237#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
1238pub enum HighlightLayer {
1239    Hint,           // lowest: pure pattern matching (paths, URLs, IPs)
1240    Tool,           // middle: backed by runtime domain knowledge (git, docker, k8s)
1241    ActionFeedback, // highest: result of an explicit user action (search, bookmarks)
1242}
1243
1244impl Default for HighlightLayer {
1245    fn default() -> Self {
1246        HighlightLayer::Hint
1247    }
1248}
1249
1250/// Style for a plugin-supplied regex highlight.
1251/// Theme-based variants reference `style.colors.text_unselected.*`.
1252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1253pub enum HighlightStyle {
1254    None,      // no color override — use with bold/italic/underline for style-only highlights
1255    Emphasis0, // fg = emphasis_0, no bg override
1256    Emphasis1, // fg = emphasis_1, no bg override
1257    Emphasis2, // fg = emphasis_2, no bg override
1258    Emphasis3, // fg = emphasis_3, no bg override
1259    BackgroundEmphasis0, // bg = emphasis_0, fg = background
1260    BackgroundEmphasis1, // bg = emphasis_1, fg = background
1261    BackgroundEmphasis2, // bg = emphasis_2, fg = background
1262    BackgroundEmphasis3, // bg = emphasis_3, fg = background
1263    CustomRgb {
1264        fg: Option<(u8, u8, u8)>,
1265        bg: Option<(u8, u8, u8)>,
1266    },
1267    CustomIndex {
1268        fg: Option<u8>,
1269        bg: Option<u8>,
1270    },
1271}
1272
1273/// One pattern + style pair sent by a plugin.
1274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1275pub struct RegexHighlight {
1276    pub pattern: String, // key for upsert; also the regex source
1277    pub style: HighlightStyle,
1278    pub layer: HighlightLayer,
1279    pub context: BTreeMap<String, String>, // arbitrary data echoed back verbatim on click
1280    pub on_hover: bool, // if true, only rendered when the cursor overlaps this match
1281    pub bold: bool,
1282    pub italic: bool,
1283    pub underline: bool,
1284    pub tooltip_text: Option<String>, // shown at bottom of pane frame when hovering over match
1285}
1286
1287// these are used for the web client
1288impl PaletteColor {
1289    pub fn as_rgb_str(&self) -> String {
1290        let (r, g, b) = match *self {
1291            Self::Rgb((r, g, b)) => (r, g, b),
1292            Self::EightBit(c) => eightbit_to_rgb(c),
1293        };
1294        format!("rgb({}, {}, {})", r, g, b)
1295    }
1296    pub fn from_rgb_str(rgb_str: &str) -> Self {
1297        let trimmed = rgb_str.trim();
1298
1299        if !trimmed.starts_with("rgb(") || !trimmed.ends_with(')') {
1300            return Self::default();
1301        }
1302
1303        let inner = trimmed
1304            .strip_prefix("rgb(")
1305            .and_then(|s| s.strip_suffix(')'))
1306            .unwrap_or("");
1307
1308        let parts: Vec<&str> = inner.split(',').collect();
1309
1310        if parts.len() != 3 {
1311            return Self::default();
1312        }
1313
1314        let mut rgb_values = [0u8; 3];
1315        for (i, part) in parts.iter().enumerate() {
1316            if let Some(rgb_val) = rgb_values.get_mut(i) {
1317                if let Ok(parsed) = part.trim().parse::<u8>() {
1318                    *rgb_val = parsed;
1319                } else {
1320                    return Self::default();
1321                }
1322            }
1323        }
1324
1325        Self::Rgb((rgb_values[0], rgb_values[1], rgb_values[2]))
1326    }
1327}
1328
1329impl FromStr for InputMode {
1330    type Err = ConversionError;
1331
1332    fn from_str(s: &str) -> Result<Self, ConversionError> {
1333        match s {
1334            "normal" | "Normal" => Ok(InputMode::Normal),
1335            "locked" | "Locked" => Ok(InputMode::Locked),
1336            "resize" | "Resize" => Ok(InputMode::Resize),
1337            "pane" | "Pane" => Ok(InputMode::Pane),
1338            "tab" | "Tab" => Ok(InputMode::Tab),
1339            "search" | "Search" => Ok(InputMode::Search),
1340            "scroll" | "Scroll" => Ok(InputMode::Scroll),
1341            "renametab" | "RenameTab" => Ok(InputMode::RenameTab),
1342            "renamepane" | "RenamePane" => Ok(InputMode::RenamePane),
1343            "session" | "Session" => Ok(InputMode::Session),
1344            "move" | "Move" => Ok(InputMode::Move),
1345            "prompt" | "Prompt" => Ok(InputMode::Prompt),
1346            "tmux" | "Tmux" => Ok(InputMode::Tmux),
1347            "entersearch" | "Entersearch" | "EnterSearch" => Ok(InputMode::EnterSearch),
1348            e => Err(ConversionError::UnknownInputMode(e.into())),
1349        }
1350    }
1351}
1352
1353#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
1354pub enum PaletteSource {
1355    Default,
1356    Xresources,
1357}
1358impl Default for PaletteSource {
1359    fn default() -> PaletteSource {
1360        PaletteSource::Default
1361    }
1362}
1363#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
1364pub struct Palette {
1365    pub source: PaletteSource,
1366    pub theme_hue: ThemeHue,
1367    pub fg: PaletteColor,
1368    pub bg: PaletteColor,
1369    pub black: PaletteColor,
1370    pub red: PaletteColor,
1371    pub green: PaletteColor,
1372    pub yellow: PaletteColor,
1373    pub blue: PaletteColor,
1374    pub magenta: PaletteColor,
1375    pub cyan: PaletteColor,
1376    pub white: PaletteColor,
1377    pub orange: PaletteColor,
1378    pub gray: PaletteColor,
1379    pub purple: PaletteColor,
1380    pub gold: PaletteColor,
1381    pub silver: PaletteColor,
1382    pub pink: PaletteColor,
1383    pub brown: PaletteColor,
1384}
1385
1386#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
1387pub struct Style {
1388    pub colors: Styling,
1389    pub rounded_corners: bool,
1390    pub hide_session_name: bool,
1391}
1392
1393#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
1394pub enum Coloration {
1395    NoStyling,
1396    Styled(StyleDeclaration),
1397}
1398
1399impl Coloration {
1400    pub fn with_fallback(&self, fallback: StyleDeclaration) -> StyleDeclaration {
1401        match &self {
1402            Coloration::NoStyling => fallback,
1403            Coloration::Styled(style) => *style,
1404        }
1405    }
1406}
1407
1408#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
1409pub struct Styling {
1410    pub text_unselected: StyleDeclaration,
1411    pub text_selected: StyleDeclaration,
1412    pub ribbon_unselected: StyleDeclaration,
1413    pub ribbon_selected: StyleDeclaration,
1414    pub table_title: StyleDeclaration,
1415    pub table_cell_unselected: StyleDeclaration,
1416    pub table_cell_selected: StyleDeclaration,
1417    pub list_unselected: StyleDeclaration,
1418    pub list_selected: StyleDeclaration,
1419    pub frame_unselected: Option<StyleDeclaration>,
1420    pub frame_selected: StyleDeclaration,
1421    pub frame_highlight: StyleDeclaration,
1422    pub exit_code_success: StyleDeclaration,
1423    pub exit_code_error: StyleDeclaration,
1424    pub multiplayer_user_colors: MultiplayerColors,
1425}
1426
1427#[derive(Debug, Copy, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
1428pub struct StyleDeclaration {
1429    pub base: PaletteColor,
1430    pub background: PaletteColor,
1431    pub emphasis_0: PaletteColor,
1432    pub emphasis_1: PaletteColor,
1433    pub emphasis_2: PaletteColor,
1434    pub emphasis_3: PaletteColor,
1435}
1436
1437#[derive(Debug, Copy, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
1438pub struct MultiplayerColors {
1439    pub player_1: PaletteColor,
1440    pub player_2: PaletteColor,
1441    pub player_3: PaletteColor,
1442    pub player_4: PaletteColor,
1443    pub player_5: PaletteColor,
1444    pub player_6: PaletteColor,
1445    pub player_7: PaletteColor,
1446    pub player_8: PaletteColor,
1447    pub player_9: PaletteColor,
1448    pub player_10: PaletteColor,
1449}
1450
1451pub const DEFAULT_STYLES: Styling = Styling {
1452    text_unselected: StyleDeclaration {
1453        base: PaletteColor::EightBit(default_colors::BRIGHT_GRAY),
1454        emphasis_0: PaletteColor::EightBit(default_colors::ORANGE),
1455        emphasis_1: PaletteColor::EightBit(default_colors::CYAN),
1456        emphasis_2: PaletteColor::EightBit(default_colors::GREEN),
1457        emphasis_3: PaletteColor::EightBit(default_colors::MAGENTA),
1458        background: PaletteColor::EightBit(default_colors::GRAY),
1459    },
1460    text_selected: StyleDeclaration {
1461        base: PaletteColor::EightBit(default_colors::BRIGHT_GRAY),
1462        emphasis_0: PaletteColor::EightBit(default_colors::ORANGE),
1463        emphasis_1: PaletteColor::EightBit(default_colors::CYAN),
1464        emphasis_2: PaletteColor::EightBit(default_colors::GREEN),
1465        emphasis_3: PaletteColor::EightBit(default_colors::MAGENTA),
1466        background: PaletteColor::EightBit(default_colors::GRAY),
1467    },
1468    ribbon_unselected: StyleDeclaration {
1469        base: PaletteColor::EightBit(default_colors::BLACK),
1470        emphasis_0: PaletteColor::EightBit(default_colors::RED),
1471        emphasis_1: PaletteColor::EightBit(default_colors::WHITE),
1472        emphasis_2: PaletteColor::EightBit(default_colors::BLUE),
1473        emphasis_3: PaletteColor::EightBit(default_colors::MAGENTA),
1474        background: PaletteColor::EightBit(default_colors::GRAY),
1475    },
1476    ribbon_selected: StyleDeclaration {
1477        base: PaletteColor::EightBit(default_colors::BLACK),
1478        emphasis_0: PaletteColor::EightBit(default_colors::RED),
1479        emphasis_1: PaletteColor::EightBit(default_colors::ORANGE),
1480        emphasis_2: PaletteColor::EightBit(default_colors::MAGENTA),
1481        emphasis_3: PaletteColor::EightBit(default_colors::BLUE),
1482        background: PaletteColor::EightBit(default_colors::GREEN),
1483    },
1484    exit_code_success: StyleDeclaration {
1485        base: PaletteColor::EightBit(default_colors::GREEN),
1486        emphasis_0: PaletteColor::EightBit(default_colors::CYAN),
1487        emphasis_1: PaletteColor::EightBit(default_colors::BLACK),
1488        emphasis_2: PaletteColor::EightBit(default_colors::MAGENTA),
1489        emphasis_3: PaletteColor::EightBit(default_colors::BLUE),
1490        background: PaletteColor::EightBit(default_colors::GRAY),
1491    },
1492    exit_code_error: StyleDeclaration {
1493        base: PaletteColor::EightBit(default_colors::RED),
1494        emphasis_0: PaletteColor::EightBit(default_colors::YELLOW),
1495        emphasis_1: PaletteColor::EightBit(default_colors::GOLD),
1496        emphasis_2: PaletteColor::EightBit(default_colors::SILVER),
1497        emphasis_3: PaletteColor::EightBit(default_colors::PURPLE),
1498        background: PaletteColor::EightBit(default_colors::GRAY),
1499    },
1500    frame_unselected: None,
1501    frame_selected: StyleDeclaration {
1502        base: PaletteColor::EightBit(default_colors::GREEN),
1503        emphasis_0: PaletteColor::EightBit(default_colors::ORANGE),
1504        emphasis_1: PaletteColor::EightBit(default_colors::CYAN),
1505        emphasis_2: PaletteColor::EightBit(default_colors::MAGENTA),
1506        emphasis_3: PaletteColor::EightBit(default_colors::BROWN),
1507        background: PaletteColor::EightBit(default_colors::GRAY),
1508    },
1509    frame_highlight: StyleDeclaration {
1510        base: PaletteColor::EightBit(default_colors::ORANGE),
1511        emphasis_0: PaletteColor::EightBit(default_colors::MAGENTA),
1512        emphasis_1: PaletteColor::EightBit(default_colors::PURPLE),
1513        emphasis_2: PaletteColor::EightBit(default_colors::GREEN),
1514        emphasis_3: PaletteColor::EightBit(default_colors::GREEN),
1515        background: PaletteColor::EightBit(default_colors::GREEN),
1516    },
1517    table_title: StyleDeclaration {
1518        base: PaletteColor::EightBit(default_colors::GREEN),
1519        emphasis_0: PaletteColor::EightBit(default_colors::ORANGE),
1520        emphasis_1: PaletteColor::EightBit(default_colors::CYAN),
1521        emphasis_2: PaletteColor::EightBit(default_colors::GREEN),
1522        emphasis_3: PaletteColor::EightBit(default_colors::MAGENTA),
1523        background: PaletteColor::EightBit(default_colors::GRAY),
1524    },
1525    table_cell_unselected: StyleDeclaration {
1526        base: PaletteColor::EightBit(default_colors::BRIGHT_GRAY),
1527        emphasis_0: PaletteColor::EightBit(default_colors::ORANGE),
1528        emphasis_1: PaletteColor::EightBit(default_colors::CYAN),
1529        emphasis_2: PaletteColor::EightBit(default_colors::GREEN),
1530        emphasis_3: PaletteColor::EightBit(default_colors::MAGENTA),
1531        background: PaletteColor::EightBit(default_colors::GRAY),
1532    },
1533    table_cell_selected: StyleDeclaration {
1534        base: PaletteColor::EightBit(default_colors::GREEN),
1535        emphasis_0: PaletteColor::EightBit(default_colors::ORANGE),
1536        emphasis_1: PaletteColor::EightBit(default_colors::CYAN),
1537        emphasis_2: PaletteColor::EightBit(default_colors::RED),
1538        emphasis_3: PaletteColor::EightBit(default_colors::MAGENTA),
1539        background: PaletteColor::EightBit(default_colors::GRAY),
1540    },
1541    list_unselected: StyleDeclaration {
1542        base: PaletteColor::EightBit(default_colors::BRIGHT_GRAY),
1543        emphasis_0: PaletteColor::EightBit(default_colors::ORANGE),
1544        emphasis_1: PaletteColor::EightBit(default_colors::CYAN),
1545        emphasis_2: PaletteColor::EightBit(default_colors::GREEN),
1546        emphasis_3: PaletteColor::EightBit(default_colors::MAGENTA),
1547        background: PaletteColor::EightBit(default_colors::GRAY),
1548    },
1549    list_selected: StyleDeclaration {
1550        base: PaletteColor::EightBit(default_colors::GREEN),
1551        emphasis_0: PaletteColor::EightBit(default_colors::ORANGE),
1552        emphasis_1: PaletteColor::EightBit(default_colors::CYAN),
1553        emphasis_2: PaletteColor::EightBit(default_colors::RED),
1554        emphasis_3: PaletteColor::EightBit(default_colors::MAGENTA),
1555        background: PaletteColor::EightBit(default_colors::GRAY),
1556    },
1557    multiplayer_user_colors: MultiplayerColors {
1558        player_1: PaletteColor::EightBit(default_colors::MAGENTA),
1559        player_2: PaletteColor::EightBit(default_colors::BLUE),
1560        player_3: PaletteColor::EightBit(default_colors::PURPLE),
1561        player_4: PaletteColor::EightBit(default_colors::YELLOW),
1562        player_5: PaletteColor::EightBit(default_colors::CYAN),
1563        player_6: PaletteColor::EightBit(default_colors::GOLD),
1564        player_7: PaletteColor::EightBit(default_colors::RED),
1565        player_8: PaletteColor::EightBit(default_colors::SILVER),
1566        player_9: PaletteColor::EightBit(default_colors::PINK),
1567        player_10: PaletteColor::EightBit(default_colors::BROWN),
1568    },
1569};
1570
1571impl Default for Styling {
1572    fn default() -> Self {
1573        DEFAULT_STYLES
1574    }
1575}
1576
1577impl From<Styling> for Palette {
1578    fn from(styling: Styling) -> Self {
1579        Palette {
1580            theme_hue: ThemeHue::Dark,
1581            source: PaletteSource::Default,
1582            fg: styling.ribbon_unselected.background,
1583            bg: styling.text_unselected.background,
1584            red: styling.exit_code_error.base,
1585            green: styling.text_unselected.emphasis_2,
1586            yellow: styling.exit_code_error.emphasis_0,
1587            blue: styling.ribbon_unselected.emphasis_2,
1588            magenta: styling.text_unselected.emphasis_3,
1589            orange: styling.text_unselected.emphasis_0,
1590            cyan: styling.text_unselected.emphasis_1,
1591            black: styling.ribbon_unselected.base,
1592            white: styling.ribbon_unselected.emphasis_1,
1593            gray: styling.list_unselected.background,
1594            purple: styling.multiplayer_user_colors.player_3,
1595            gold: styling.multiplayer_user_colors.player_6,
1596            silver: styling.multiplayer_user_colors.player_8,
1597            pink: styling.multiplayer_user_colors.player_9,
1598            brown: styling.multiplayer_user_colors.player_10,
1599        }
1600    }
1601}
1602
1603impl From<Palette> for Styling {
1604    fn from(palette: Palette) -> Self {
1605        let (fg, bg) = match palette.theme_hue {
1606            ThemeHue::Light => (palette.black, palette.white),
1607            ThemeHue::Dark => (palette.white, palette.black),
1608        };
1609        Styling {
1610            text_unselected: StyleDeclaration {
1611                base: fg,
1612                emphasis_0: palette.orange,
1613                emphasis_1: palette.cyan,
1614                emphasis_2: palette.green,
1615                emphasis_3: palette.magenta,
1616                background: bg,
1617            },
1618            text_selected: StyleDeclaration {
1619                base: fg,
1620                emphasis_0: palette.orange,
1621                emphasis_1: palette.cyan,
1622                emphasis_2: palette.green,
1623                emphasis_3: palette.magenta,
1624                background: palette.bg,
1625            },
1626            ribbon_unselected: StyleDeclaration {
1627                base: palette.black,
1628                emphasis_0: palette.red,
1629                emphasis_1: palette.white,
1630                emphasis_2: palette.blue,
1631                emphasis_3: palette.magenta,
1632                background: palette.fg,
1633            },
1634            ribbon_selected: StyleDeclaration {
1635                base: palette.black,
1636                emphasis_0: palette.red,
1637                emphasis_1: palette.orange,
1638                emphasis_2: palette.magenta,
1639                emphasis_3: palette.blue,
1640                background: palette.green,
1641            },
1642            exit_code_success: StyleDeclaration {
1643                base: palette.green,
1644                emphasis_0: palette.cyan,
1645                emphasis_1: palette.black,
1646                emphasis_2: palette.magenta,
1647                emphasis_3: palette.blue,
1648                background: Default::default(),
1649            },
1650            exit_code_error: StyleDeclaration {
1651                base: palette.red,
1652                emphasis_0: palette.yellow,
1653                emphasis_1: palette.gold,
1654                emphasis_2: palette.silver,
1655                emphasis_3: palette.purple,
1656                background: Default::default(),
1657            },
1658            frame_unselected: None,
1659            frame_selected: StyleDeclaration {
1660                base: palette.green,
1661                emphasis_0: palette.orange,
1662                emphasis_1: palette.cyan,
1663                emphasis_2: palette.magenta,
1664                emphasis_3: palette.brown,
1665                background: Default::default(),
1666            },
1667            frame_highlight: StyleDeclaration {
1668                base: palette.orange,
1669                emphasis_0: palette.magenta,
1670                emphasis_1: palette.purple,
1671                emphasis_2: palette.orange,
1672                emphasis_3: palette.orange,
1673                background: Default::default(),
1674            },
1675            table_title: StyleDeclaration {
1676                base: palette.green,
1677                emphasis_0: palette.orange,
1678                emphasis_1: palette.cyan,
1679                emphasis_2: palette.green,
1680                emphasis_3: palette.magenta,
1681                background: palette.gray,
1682            },
1683            table_cell_unselected: StyleDeclaration {
1684                base: fg,
1685                emphasis_0: palette.orange,
1686                emphasis_1: palette.cyan,
1687                emphasis_2: palette.green,
1688                emphasis_3: palette.magenta,
1689                background: palette.black,
1690            },
1691            table_cell_selected: StyleDeclaration {
1692                base: fg,
1693                emphasis_0: palette.orange,
1694                emphasis_1: palette.cyan,
1695                emphasis_2: palette.green,
1696                emphasis_3: palette.magenta,
1697                background: palette.bg,
1698            },
1699            list_unselected: StyleDeclaration {
1700                base: palette.white,
1701                emphasis_0: palette.orange,
1702                emphasis_1: palette.cyan,
1703                emphasis_2: palette.green,
1704                emphasis_3: palette.magenta,
1705                background: palette.black,
1706            },
1707            list_selected: StyleDeclaration {
1708                base: palette.white,
1709                emphasis_0: palette.orange,
1710                emphasis_1: palette.cyan,
1711                emphasis_2: palette.green,
1712                emphasis_3: palette.magenta,
1713                background: palette.bg,
1714            },
1715            multiplayer_user_colors: MultiplayerColors {
1716                player_1: palette.magenta,
1717                player_2: palette.blue,
1718                player_3: palette.purple,
1719                player_4: palette.yellow,
1720                player_5: palette.cyan,
1721                player_6: palette.gold,
1722                player_7: palette.red,
1723                player_8: palette.silver,
1724                player_9: palette.pink,
1725                player_10: palette.brown,
1726            },
1727        }
1728    }
1729}
1730
1731// FIXME: Poor devs hashtable since HashTable can't derive `Default`...
1732pub type KeybindsVec = Vec<(InputMode, Vec<(KeyWithModifier, Vec<Action>)>)>;
1733
1734/// Provides information helpful in rendering the Zellij controls for UI bars
1735#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1736pub struct ModeInfo {
1737    pub mode: InputMode,
1738    pub base_mode: Option<InputMode>,
1739    pub keybinds: KeybindsVec,
1740    pub style: Style,
1741    pub capabilities: PluginCapabilities,
1742    pub session_name: Option<String>,
1743    pub editor: Option<PathBuf>,
1744    pub shell: Option<PathBuf>,
1745    pub web_clients_allowed: Option<bool>,
1746    pub web_sharing: Option<WebSharing>,
1747    pub currently_marking_pane_group: Option<bool>,
1748    pub is_web_client: Option<bool>,
1749    // note: these are only the configured ip/port that will be bound if and when the server is up
1750    pub web_server_ip: Option<IpAddr>,
1751    pub web_server_port: Option<u16>,
1752    pub web_server_capability: Option<bool>,
1753    pub pane_frame_style: Option<PaneFrameStyle>,
1754    pub session_dimmed: Option<bool>,
1755    pub session_ancestry: Vec<String>,
1756    pub host_fullscreen: Option<bool>,
1757    pub nested_ascend_keys: Vec<KeyWithModifier>,
1758    pub session_ascended: Option<bool>,
1759    pub nested_descend_keys: Vec<KeyWithModifier>,
1760}
1761
1762impl ModeInfo {
1763    pub fn get_mode_keybinds(&self) -> Vec<(KeyWithModifier, Vec<Action>)> {
1764        self.get_keybinds_for_mode(self.mode)
1765    }
1766
1767    pub fn get_keybinds_for_mode(&self, mode: InputMode) -> Vec<(KeyWithModifier, Vec<Action>)> {
1768        for (vec_mode, map) in &self.keybinds {
1769            if mode == *vec_mode {
1770                return map.to_vec();
1771            }
1772        }
1773        vec![]
1774    }
1775    pub fn update_keybinds(&mut self, keybinds: Keybinds) {
1776        self.keybinds = keybinds.to_keybinds_vec();
1777    }
1778    pub fn update_default_mode(&mut self, new_default_mode: InputMode) {
1779        self.base_mode = Some(new_default_mode);
1780    }
1781    pub fn update_theme(&mut self, theme: Styling) {
1782        self.style.colors = theme.into();
1783    }
1784    pub fn update_rounded_corners(&mut self, rounded_corners: bool) {
1785        self.style.rounded_corners = rounded_corners;
1786    }
1787    pub fn update_arrow_fonts(&mut self, should_support_arrow_fonts: bool) {
1788        // it is honestly quite baffling to me how "arrow_fonts: false" can mean "I support arrow
1789        // fonts", but since this is a public API... ¯\_(ツ)_/¯
1790        self.capabilities.arrow_fonts = !should_support_arrow_fonts;
1791    }
1792    pub fn update_hide_session_name(&mut self, hide_session_name: bool) {
1793        self.style.hide_session_name = hide_session_name;
1794    }
1795    pub fn change_to_default_mode(&mut self) {
1796        if let Some(base_mode) = self.base_mode {
1797            self.mode = base_mode;
1798        }
1799    }
1800}
1801
1802#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
1803pub struct SessionInfo {
1804    pub name: String,
1805    pub tabs: Vec<TabInfo>,
1806    pub panes: PaneManifest,
1807    pub connected_clients: usize,
1808    pub is_current_session: bool,
1809    pub available_layouts: Vec<LayoutInfo>,
1810    pub plugins: BTreeMap<u32, PluginInfo>,
1811    pub web_clients_allowed: bool,
1812    pub web_client_count: usize,
1813    pub tab_history: BTreeMap<ClientId, Vec<usize>>,
1814    pub pane_history: BTreeMap<ClientId, Vec<PaneId>>,
1815    pub creation_time: Duration,
1816}
1817
1818#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
1819pub struct PluginInfo {
1820    pub location: String,
1821    pub configuration: BTreeMap<String, String>,
1822}
1823
1824impl From<RunPlugin> for PluginInfo {
1825    fn from(run_plugin: RunPlugin) -> Self {
1826        PluginInfo {
1827            location: run_plugin.location.display(),
1828            configuration: run_plugin.configuration.inner().clone(),
1829        }
1830    }
1831}
1832
1833#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1834pub enum LayoutInfo {
1835    BuiltIn(String),
1836    File(String, LayoutMetadata),
1837    Url(String),
1838    Stringified(String),
1839}
1840
1841#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1842pub struct LayoutWithError {
1843    pub layout_name: String,
1844    pub error: LayoutParsingError,
1845}
1846
1847#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1848pub enum LayoutParsingError {
1849    KdlError {
1850        kdl_error: KdlError,
1851        file_name: String,
1852        source_code: String,
1853    },
1854    SyntaxError,
1855}
1856
1857impl AsRef<LayoutInfo> for LayoutInfo {
1858    fn as_ref(&self) -> &LayoutInfo {
1859        self
1860    }
1861}
1862
1863#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
1864pub struct LayoutMetadata {
1865    pub tabs: Vec<TabMetadata>,
1866    pub creation_time: String,
1867    pub update_time: String,
1868}
1869
1870impl From<&PathBuf> for LayoutMetadata {
1871    fn from(path: &PathBuf) -> LayoutMetadata {
1872        match Layout::stringified_from_path(path) {
1873            Ok((path_str, stringified_layout, _swap_layouts)) => {
1874                match Layout::from_kdl(&stringified_layout, Some(path_str), None, None) {
1875                    Ok(layout) => {
1876                        let layout_tabs = layout.tabs();
1877                        let tabs = if layout_tabs.is_empty() {
1878                            let (tiled_pane_layout, floating_pane_layout) = layout.new_tab();
1879                            vec![TabMetadata::from(&(
1880                                None,
1881                                tiled_pane_layout,
1882                                floating_pane_layout,
1883                            ))]
1884                        } else {
1885                            layout
1886                                .tabs()
1887                                .into_iter()
1888                                .map(|tab| TabMetadata::from(&tab))
1889                                .collect()
1890                        };
1891
1892                        // Get file metadata for creation and modification times as Unix epochs
1893                        let (creation_time, update_time) =
1894                            LayoutMetadata::creation_and_update_times(&path);
1895
1896                        LayoutMetadata {
1897                            tabs,
1898                            creation_time,
1899                            update_time,
1900                        }
1901                    },
1902                    Err(e) => {
1903                        log::error!("Failed to parse layout: {}", e);
1904                        LayoutMetadata::default()
1905                    },
1906                }
1907            },
1908            Err(e) => {
1909                log::error!("Failed to read layout file: {}", e);
1910                LayoutMetadata::default()
1911            },
1912        }
1913    }
1914}
1915
1916impl LayoutMetadata {
1917    fn creation_and_update_times(path: &PathBuf) -> (String, String) {
1918        // (creation_time, update_time) returns stringified unix epoch
1919        match std::fs::metadata(path) {
1920            Ok(metadata) => {
1921                let creation_time = metadata
1922                    .created()
1923                    .ok()
1924                    .and_then(|t| {
1925                        t.duration_since(std::time::UNIX_EPOCH)
1926                            .ok()
1927                            .map(|d| d.as_secs().to_string())
1928                    })
1929                    .unwrap_or_default();
1930
1931                let update_time = metadata
1932                    .modified()
1933                    .ok()
1934                    .and_then(|t| {
1935                        t.duration_since(std::time::UNIX_EPOCH)
1936                            .ok()
1937                            .map(|d| d.as_secs().to_string())
1938                    })
1939                    .unwrap_or_default();
1940
1941                (creation_time, update_time)
1942            },
1943            Err(_) => (String::new(), String::new()),
1944        }
1945    }
1946}
1947
1948#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1949pub struct TabMetadata {
1950    pub panes: Vec<PaneMetadata>,
1951    pub name: Option<String>,
1952}
1953
1954impl
1955    From<&(
1956        Option<String>,
1957        crate::input::layout::TiledPaneLayout,
1958        Vec<crate::input::layout::FloatingPaneLayout>,
1959    )> for TabMetadata
1960{
1961    fn from(
1962        tab: &(
1963            Option<String>,
1964            crate::input::layout::TiledPaneLayout,
1965            Vec<crate::input::layout::FloatingPaneLayout>,
1966        ),
1967    ) -> Self {
1968        let (tab_name, tiled_pane_layout, floating_panes) = tab;
1969
1970        // Collect panes from tiled layout (only leaf nodes are real panes)
1971        let mut panes = Vec::new();
1972        collect_leaf_panes(&tiled_pane_layout, &mut panes);
1973
1974        // Collect panes from floating panes
1975        for floating_pane in floating_panes {
1976            panes.push(PaneMetadata::from(floating_pane));
1977        }
1978
1979        TabMetadata {
1980            panes,
1981            name: tab_name.clone(),
1982        }
1983    }
1984}
1985
1986#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1987pub struct PaneMetadata {
1988    pub name: Option<String>,
1989    pub is_plugin: bool,
1990    pub is_builtin_plugin: bool,
1991}
1992
1993impl From<&crate::input::layout::TiledPaneLayout> for PaneMetadata {
1994    fn from(pane: &crate::input::layout::TiledPaneLayout) -> Self {
1995        let mut is_plugin = false;
1996        let mut is_builtin_plugin = false;
1997
1998        // Try to get the name from the pane's name field first
1999        let name = if let Some(ref name) = pane.name {
2000            Some(name.clone())
2001        } else if let Some(ref run) = pane.run {
2002            // If no explicit name, glean it from the run configuration
2003            match run {
2004                Run::Command(cmd) => {
2005                    // Use the command name
2006                    Some(cmd.command.to_string_lossy().to_string())
2007                },
2008                Run::EditFile(path, _line, _cwd) => {
2009                    // Use the file name
2010                    path.file_name().map(|n| n.to_string_lossy().to_string())
2011                },
2012                Run::Plugin(plugin) => {
2013                    is_plugin = true;
2014                    is_builtin_plugin = plugin.is_builtin_plugin();
2015                    Some(plugin.location_string())
2016                },
2017                Run::Cwd(_) => None,
2018            }
2019        } else {
2020            None
2021        };
2022
2023        PaneMetadata {
2024            name,
2025            is_plugin,
2026            is_builtin_plugin,
2027        }
2028    }
2029}
2030
2031impl From<&crate::input::layout::FloatingPaneLayout> for PaneMetadata {
2032    fn from(pane: &crate::input::layout::FloatingPaneLayout) -> Self {
2033        let mut is_plugin = false;
2034        let mut is_builtin_plugin = false;
2035
2036        // Try to get the name from the pane's name field first
2037        let name = if let Some(ref name) = pane.name {
2038            Some(name.clone())
2039        } else if let Some(ref run) = pane.run {
2040            // If no explicit name, glean it from the run configuration
2041            match run {
2042                Run::Command(cmd) => {
2043                    // Use the command name
2044                    Some(cmd.command.to_string_lossy().to_string())
2045                },
2046                Run::EditFile(path, _line, _cwd) => {
2047                    // Use the file name
2048                    path.file_name().map(|n| n.to_string_lossy().to_string())
2049                },
2050                Run::Plugin(plugin) => {
2051                    is_plugin = true;
2052                    is_builtin_plugin = match plugin {
2053                        crate::input::layout::RunPluginOrAlias::RunPlugin(run_plugin) => {
2054                            matches!(run_plugin.location, RunPluginLocation::Zellij(_))
2055                        },
2056                        crate::input::layout::RunPluginOrAlias::Alias(_) => false,
2057                    };
2058                    // Use the plugin location string
2059                    Some(plugin.location_string())
2060                },
2061                Run::Cwd(_) => None,
2062            }
2063        } else {
2064            None
2065        };
2066
2067        PaneMetadata {
2068            name,
2069            is_plugin,
2070            is_builtin_plugin,
2071        }
2072    }
2073}
2074
2075// Helper function to recursively collect leaf panes from TiledPaneLayout
2076fn collect_leaf_panes(
2077    pane: &crate::input::layout::TiledPaneLayout,
2078    result: &mut Vec<PaneMetadata>,
2079) {
2080    if pane.children.is_empty() {
2081        // This is a leaf node (actual pane)
2082        result.push(PaneMetadata::from(pane));
2083    } else {
2084        // This is a container, recurse into children
2085        for child in &pane.children {
2086            collect_leaf_panes(child, result);
2087        }
2088    }
2089}
2090
2091impl LayoutInfo {
2092    pub fn name(&self) -> &str {
2093        match self {
2094            LayoutInfo::BuiltIn(name) => &name,
2095            LayoutInfo::File(name, _) => &name,
2096            LayoutInfo::Url(url) => &url,
2097            LayoutInfo::Stringified(layout) => &layout,
2098        }
2099    }
2100    pub fn is_builtin(&self) -> bool {
2101        match self {
2102            LayoutInfo::BuiltIn(_name) => true,
2103            LayoutInfo::File(_name, _) => false,
2104            LayoutInfo::Url(_url) => false,
2105            LayoutInfo::Stringified(_stringified) => false,
2106        }
2107    }
2108    pub fn from_cli(
2109        layout_dir: &Option<PathBuf>,
2110        maybe_layout_path: &Option<PathBuf>,
2111        cwd: PathBuf,
2112    ) -> Option<Self> {
2113        // If we're not given a layout path, fall back to "default". Since we cannot tell ahead of
2114        // time whether the user has a layout named "default.kdl" in their layout directory, we
2115        // cannot blindly assume that this is indeed the builtin default layout. The layout
2116        // resolution below will correctly handle this.
2117        // The docs promise this behavior, so we have to abide:
2118        // <https://zellij.dev/documentation/layouts.html#layout-default-directory>
2119        let layout_path = maybe_layout_path
2120            .clone()
2121            .unwrap_or(PathBuf::from("default"));
2122
2123        if layout_path.starts_with("http://") || layout_path.starts_with("https://") {
2124            Some(LayoutInfo::Url(layout_path.display().to_string()))
2125        } else if layout_path.extension().is_some() || layout_path.components().count() > 1 {
2126            let layout_dir = cwd;
2127            let file_path = layout_dir.join(layout_path);
2128            Some(LayoutInfo::File(
2129                // layout_dir.join(layout_path).display().to_string(),
2130                file_path.display().to_string(),
2131                LayoutMetadata::from(&file_path),
2132            ))
2133        } else {
2134            // Attempt to interpret the layout as bare layout name from the layout application
2135            // directory. This is described in the docs:
2136            // <https://zellij.dev/documentation/layouts.html#layout-default-directory>
2137            if let Some(layout_dir) = layout_dir
2138                .as_ref()
2139                .map(|l| l.clone())
2140                .or_else(default_layout_dir)
2141            {
2142                let file_path = layout_dir.join(&layout_path);
2143                if file_path.exists() {
2144                    return Some(LayoutInfo::File(
2145                        file_path.display().to_string(),
2146                        LayoutMetadata::from(&file_path),
2147                    ));
2148                }
2149                let file_path_with_ext = file_path.with_extension("kdl");
2150                if file_path_with_ext.exists() {
2151                    return Some(LayoutInfo::File(
2152                        file_path_with_ext.display().to_string(),
2153                        LayoutMetadata::from(&file_path_with_ext),
2154                    ));
2155                }
2156            }
2157            // Assume a builtin layout by default
2158            Some(LayoutInfo::BuiltIn(layout_path.display().to_string()))
2159        }
2160    }
2161    pub fn from_config(
2162        layout_dir: &Option<PathBuf>,
2163        maybe_layout_path: &Option<PathBuf>,
2164    ) -> Option<Self> {
2165        // If we're not given a layout path, fall back to "default". Since we cannot tell ahead of
2166        // time whether the user has a layout named "default.kdl" in their layout directory, we
2167        // cannot blindly assume that this is indeed the builtin default layout. The layout
2168        // resolution below will correctly handle this.
2169        // The docs promise this behavior, so we have to abide:
2170        // <https://zellij.dev/documentation/layouts.html#layout-default-directory>
2171        let layout_path = maybe_layout_path
2172            .clone()
2173            .unwrap_or(PathBuf::from("default"));
2174
2175        if layout_path.starts_with("http://") || layout_path.starts_with("https://") {
2176            Some(LayoutInfo::Url(layout_path.display().to_string()))
2177        } else if layout_path.extension().is_some() || layout_path.components().count() > 1 {
2178            let Some(layout_dir) = layout_dir
2179                .as_ref()
2180                .map(|l| l.clone())
2181                .or_else(default_layout_dir)
2182            else {
2183                return None;
2184            };
2185            let file_path = layout_dir.join(layout_path);
2186            Some(LayoutInfo::File(
2187                // layout_dir.join(layout_path).display().to_string(),
2188                file_path.display().to_string(),
2189                LayoutMetadata::from(&file_path),
2190            ))
2191        } else {
2192            // Attempt to interpret the layout as bare layout name from the layout application
2193            // directory. This is described in the docs:
2194            // <https://zellij.dev/documentation/layouts.html#layout-default-directory>
2195            if let Some(layout_dir) = layout_dir
2196                .as_ref()
2197                .map(|l| l.clone())
2198                .or_else(default_layout_dir)
2199            {
2200                let file_path = layout_dir.join(&layout_path);
2201                if file_path.exists() {
2202                    return Some(LayoutInfo::File(
2203                        file_path.display().to_string(),
2204                        LayoutMetadata::from(&file_path),
2205                    ));
2206                }
2207                let file_path_with_ext = file_path.with_extension("kdl");
2208                if file_path_with_ext.exists() {
2209                    return Some(LayoutInfo::File(
2210                        file_path_with_ext.display().to_string(),
2211                        LayoutMetadata::from(&file_path_with_ext),
2212                    ));
2213                }
2214            }
2215            // Assume a builtin layout by default
2216            Some(LayoutInfo::BuiltIn(layout_path.display().to_string()))
2217        }
2218    }
2219}
2220
2221#[allow(clippy::derive_hash_xor_eq)]
2222impl Hash for SessionInfo {
2223    fn hash<H: Hasher>(&self, state: &mut H) {
2224        self.name.hash(state);
2225    }
2226}
2227
2228impl SessionInfo {
2229    pub fn new(name: String) -> Self {
2230        SessionInfo {
2231            name,
2232            ..Default::default()
2233        }
2234    }
2235    pub fn update_tab_info(&mut self, new_tab_info: Vec<TabInfo>) {
2236        self.tabs = new_tab_info;
2237    }
2238    pub fn update_pane_info(&mut self, new_pane_info: PaneManifest) {
2239        self.panes = new_pane_info;
2240    }
2241    pub fn update_connected_clients(&mut self, new_connected_clients: usize) {
2242        self.connected_clients = new_connected_clients;
2243    }
2244    pub fn populate_plugin_list(&mut self, plugins: BTreeMap<u32, RunPlugin>) {
2245        // u32 - plugin_id
2246        let mut plugin_list = BTreeMap::new();
2247        for (plugin_id, run_plugin) in plugins {
2248            plugin_list.insert(plugin_id, run_plugin.into());
2249        }
2250        self.plugins = plugin_list;
2251    }
2252}
2253
2254/// Contains all the information for a currently opened tab.
2255#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
2256pub struct TabInfo {
2257    /// The Tab's 0 indexed position
2258    pub position: usize,
2259    /// The name of the tab as it appears in the UI (if there's enough room for it)
2260    pub name: String,
2261    /// Whether this tab is focused
2262    pub active: bool,
2263    /// The number of suppressed panes this tab has
2264    pub panes_to_hide: usize,
2265    /// Whether there's one pane taking up the whole display area on this tab
2266    pub is_fullscreen_active: bool,
2267    /// Whether input sent to this tab will be synced to all panes in it
2268    pub is_sync_panes_active: bool,
2269    pub are_floating_panes_visible: bool,
2270    pub other_focused_clients: Vec<ClientId>,
2271    pub active_swap_layout_name: Option<String>,
2272    /// Whether the user manually changed the layout, moving out of the swap layout scheme
2273    pub is_swap_layout_dirty: bool,
2274    /// Row count in the viewport (including all non-ui panes, eg. will exclude the status bar)
2275    pub viewport_rows: usize,
2276    /// Column count in the viewport (including all non-ui panes, eg. will exclude the status bar)
2277    pub viewport_columns: usize,
2278    /// Row count in the display area (including all panes, will typically be larger than the
2279    /// viewport)
2280    pub display_area_rows: usize,
2281    /// Column count in the display area (including all panes, will typically be larger than the
2282    /// viewport)
2283    pub display_area_columns: usize,
2284    /// The number of selectable (eg. not the UI bars) tiled panes currently in this tab
2285    pub selectable_tiled_panes_count: usize,
2286    /// The number of selectable (eg. not the UI bars) floating panes currently in this tab
2287    pub selectable_floating_panes_count: usize,
2288    /// The stable identifier for this tab
2289    pub tab_id: usize,
2290    /// Whether this tab has an active (persistent) bell notification
2291    pub has_bell_notification: bool,
2292    /// Whether this tab is currently flashing its bell (transient 400ms state)
2293    pub is_flashing_bell: bool,
2294}
2295
2296/// The `PaneManifest` contains a dictionary of panes, indexed by the tab position (0 indexed).
2297/// Panes include all panes in the relevant tab, including `tiled` panes, `floating` panes and
2298/// `suppressed` panes.
2299#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
2300pub struct PaneManifest {
2301    pub panes: HashMap<usize, Vec<PaneInfo>>, // usize is the tab position
2302}
2303
2304/// Contains all the information for a currently open pane
2305///
2306/// # Difference between coordinates/size and content coordinates/size
2307///
2308/// The pane basic coordinates and size (eg. `pane_x` or `pane_columns`) are the entire space taken
2309/// up by this pane - including its frame and title if it has a border.
2310///
2311/// The pane content coordinates and size (eg. `pane_content_x` or `pane_content_columns`)
2312/// represent the area taken by the pane's content, excluding its frame and title if it has a
2313/// border.
2314#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
2315pub struct PaneInfo {
2316    /// The id of the pane, unique to all panes of this kind (eg. id in terminals or id in panes)
2317    pub id: u32,
2318    /// Whether this pane is a plugin (`true`) or a terminal (`false`), used along with `id` can represent a unique pane ID across
2319    /// the running session
2320    pub is_plugin: bool,
2321    /// Whether the pane is focused in its layer (tiled or floating)
2322    pub is_focused: bool,
2323    pub is_fullscreen: bool,
2324    /// Whether a pane is floating or tiled (embedded)
2325    pub is_floating: bool,
2326    /// Whether a pane is suppressed - suppressed panes are not visible to the user, but still run
2327    /// in the background
2328    pub is_suppressed: bool,
2329    /// The full title of the pane as it appears in the UI (if there is room for it)
2330    pub title: String,
2331    /// Whether a pane exited or not, note that most panes close themselves before setting this
2332    /// flag, so this is only relevant to command panes
2333    pub exited: bool,
2334    /// The exit status of a pane if it did exit and is still in the UI
2335    pub exit_status: Option<i32>,
2336    /// A "held" pane is a paused pane that is waiting for user input (eg. a command pane that
2337    /// exited and is waiting to be re-run or closed)
2338    pub is_held: bool,
2339    pub pane_x: usize,
2340    pub pane_content_x: usize,
2341    pub pane_y: usize,
2342    pub pane_content_y: usize,
2343    pub pane_rows: usize,
2344    pub pane_content_rows: usize,
2345    pub pane_columns: usize,
2346    pub pane_content_columns: usize,
2347    /// The coordinates of the cursor - if this pane is focused - relative to the pane's
2348    /// coordinates
2349    pub cursor_coordinates_in_pane: Option<(usize, usize)>, // x, y if cursor is visible
2350    /// If this is a command pane, this will show the stringified version of the command and its
2351    /// arguments
2352    pub terminal_command: Option<String>,
2353    /// The URL from which this plugin was loaded (eg. `zellij:strider` for the built-in `strider`
2354    /// plugin or `file:/path/to/my/plugin.wasm` for a local plugin)
2355    pub plugin_url: Option<String>,
2356    /// Unselectable panes are often used for UI elements that do not have direct user interaction
2357    /// (eg. the default `status-bar` or `tab-bar`).
2358    pub is_selectable: bool,
2359    /// Grouped panes (usually through an explicit user action) that are staged for a bulk action
2360    /// the index is kept track of in order to preserve the pane group order
2361    pub index_in_pane_group: BTreeMap<ClientId, usize>,
2362    /// The default foreground color of this pane, if set (e.g. "#00e000")
2363    pub default_fg: Option<String>,
2364    /// The default background color of this pane, if set (e.g. "#001a3a")
2365    pub default_bg: Option<String>,
2366}
2367
2368#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
2369pub struct PaneListEntry {
2370    #[serde(flatten)]
2371    pub pane_info: PaneInfo,
2372    pub tab_id: usize,
2373    pub tab_position: usize,
2374    pub tab_name: String,
2375    #[serde(skip_serializing_if = "Option::is_none")]
2376    pub pane_command: Option<String>,
2377    #[serde(skip_serializing_if = "Option::is_none")]
2378    pub pane_cwd: Option<String>,
2379}
2380
2381pub type ListPanesResponse = Vec<PaneListEntry>;
2382pub type ListTabsResponse = Vec<TabInfo>;
2383
2384#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
2385pub struct ClientInfo {
2386    pub client_id: ClientId,
2387    pub pane_id: PaneId,
2388    pub running_command: String,
2389    pub is_current_client: bool,
2390}
2391
2392impl ClientInfo {
2393    pub fn new(
2394        client_id: ClientId,
2395        pane_id: PaneId,
2396        running_command: String,
2397        is_current_client: bool,
2398    ) -> Self {
2399        ClientInfo {
2400            client_id,
2401            pane_id,
2402            running_command,
2403            is_current_client,
2404        }
2405    }
2406}
2407
2408#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
2409pub struct PaneRenderReport {
2410    pub all_pane_contents: HashMap<ClientId, HashMap<PaneId, PaneContents>>,
2411    pub all_pane_contents_with_ansi: HashMap<ClientId, HashMap<PaneId, PaneContents>>,
2412}
2413
2414impl PaneRenderReport {
2415    pub fn add_pane_contents(
2416        &mut self,
2417        client_ids: &[ClientId],
2418        pane_id: PaneId,
2419        pane_contents: PaneContents,
2420    ) {
2421        for client_id in client_ids {
2422            let p = self
2423                .all_pane_contents
2424                .entry(*client_id)
2425                .or_insert_with(|| HashMap::new());
2426            p.insert(pane_id, pane_contents.clone());
2427        }
2428    }
2429    pub fn add_pane_contents_with_ansi(
2430        &mut self,
2431        client_ids: &[ClientId],
2432        pane_id: PaneId,
2433        pane_contents: PaneContents,
2434    ) {
2435        for client_id in client_ids {
2436            let p = self
2437                .all_pane_contents_with_ansi
2438                .entry(*client_id)
2439                .or_insert_with(|| HashMap::new());
2440            p.insert(pane_id, pane_contents.clone());
2441        }
2442    }
2443}
2444
2445#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
2446pub struct PaneContents {
2447    // NOTE: both lines_above_viewport and lines_below_viewport are only populated if explicitly
2448    // requested (eg. with get_full_scrollback true in the plugin command) this is for performance
2449    // reasons
2450    pub lines_above_viewport: Vec<String>,
2451    pub lines_below_viewport: Vec<String>,
2452    pub viewport: Vec<String>,
2453    pub selected_text: Option<SelectedText>,
2454    pub cursor: Option<(usize, usize)>,
2455}
2456
2457/// Extract text from a line between two column positions, accounting for wide characters
2458fn extract_text_by_columns(line: &str, start_col: usize, end_col: usize) -> String {
2459    let mut current_col = 0;
2460    let mut result = String::new();
2461    let mut capturing = false;
2462
2463    for ch in line.chars() {
2464        let char_width = ch.width().unwrap_or(0);
2465
2466        // Start capturing when we reach start_col
2467        if current_col >= start_col && !capturing {
2468            capturing = true;
2469        }
2470
2471        // Stop if we've reached or passed end_col
2472        if current_col >= end_col {
2473            break;
2474        }
2475
2476        // Capture character if we're in the range
2477        if capturing {
2478            result.push(ch);
2479        }
2480
2481        current_col += char_width;
2482    }
2483
2484    result
2485}
2486
2487/// Extract text from a line starting at a column position, accounting for wide characters
2488fn extract_text_from_column(line: &str, start_col: usize) -> String {
2489    let mut current_col = 0;
2490    let mut result = String::new();
2491    let mut capturing = false;
2492
2493    for ch in line.chars() {
2494        let char_width = ch.width().unwrap_or(0);
2495
2496        if current_col >= start_col {
2497            capturing = true;
2498        }
2499
2500        if capturing {
2501            result.push(ch);
2502        }
2503
2504        current_col += char_width;
2505    }
2506
2507    result
2508}
2509
2510/// Extract text from a line up to a column position, accounting for wide characters
2511fn extract_text_to_column(line: &str, end_col: usize) -> String {
2512    let mut current_col = 0;
2513    let mut result = String::new();
2514
2515    for ch in line.chars() {
2516        let char_width = ch.width().unwrap_or(0);
2517
2518        if current_col >= end_col {
2519            break;
2520        }
2521
2522        result.push(ch);
2523        current_col += char_width;
2524    }
2525
2526    result
2527}
2528
2529impl PaneContents {
2530    pub fn new(viewport: Vec<String>, selection_start: Position, selection_end: Position) -> Self {
2531        PaneContents {
2532            viewport,
2533            selected_text: SelectedText::from_positions(selection_start, selection_end),
2534            ..Default::default()
2535        }
2536    }
2537    pub fn new_with_scrollback(
2538        viewport: Vec<String>,
2539        selection_start: Position,
2540        selection_end: Position,
2541        lines_above_viewport: Vec<String>,
2542        lines_below_viewport: Vec<String>,
2543    ) -> Self {
2544        PaneContents {
2545            viewport,
2546            selected_text: SelectedText::from_positions(selection_start, selection_end),
2547            lines_above_viewport,
2548            lines_below_viewport,
2549            cursor: None,
2550        }
2551    }
2552
2553    /// Returns the actual text content of the selection, if any exists.
2554    /// Selection only occurs within the viewport.
2555    pub fn get_selected_text(&self) -> Option<String> {
2556        let selected_text = self.selected_text?;
2557
2558        let start_line = selected_text.start.line() as usize;
2559        let start_col = selected_text.start.column();
2560        let end_line = selected_text.end.line() as usize;
2561        let end_col = selected_text.end.column();
2562
2563        // Handle out of bounds
2564        if start_line >= self.viewport.len() || end_line >= self.viewport.len() {
2565            return None;
2566        }
2567
2568        if start_line == end_line {
2569            // Single line selection
2570            let line = &self.viewport[start_line];
2571            Some(extract_text_by_columns(line, start_col, end_col))
2572        } else {
2573            // Multi-line selection
2574            let mut result = String::new();
2575
2576            // First line - from start column to end of line
2577            let first_line = &self.viewport[start_line];
2578            result.push_str(&extract_text_from_column(first_line, start_col));
2579            result.push('\n');
2580
2581            // Middle lines - complete lines
2582            for i in (start_line + 1)..end_line {
2583                result.push_str(&self.viewport[i]);
2584                result.push('\n');
2585            }
2586
2587            // Last line - from start to end column
2588            let last_line = &self.viewport[end_line];
2589            result.push_str(&extract_text_to_column(last_line, end_col));
2590
2591            Some(result)
2592        }
2593    }
2594}
2595
2596#[derive(Debug, Clone, Serialize, Deserialize)]
2597pub enum PaneScrollbackResponse {
2598    Ok(PaneContents),
2599    Err(String),
2600}
2601
2602#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2603pub enum GetPanePidResponse {
2604    Ok(i32),
2605    Err(String),
2606}
2607
2608#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2609pub enum GetPaneRunningCommandResponse {
2610    Ok(Vec<String>),
2611    Err(String),
2612}
2613
2614#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
2615pub struct SessionListSnapshot {
2616    pub live_sessions: Vec<SessionInfo>,
2617    pub resurrectable_sessions: Vec<(String, Duration)>,
2618}
2619
2620#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2621pub enum GetSessionListResponse {
2622    Ok(SessionListSnapshot),
2623    Err(String),
2624}
2625
2626#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2627pub enum KillSessionsResponse {
2628    Ok,
2629    Err(String),
2630}
2631
2632#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2633pub enum DeleteDeadSessionResponse {
2634    Ok,
2635    Err(String),
2636}
2637
2638#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2639pub enum DeleteAllDeadSessionsResponse {
2640    Ok,
2641    Err(String),
2642}
2643
2644#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2645pub enum GetPaneCwdResponse {
2646    Ok(PathBuf),
2647    Err(String),
2648}
2649
2650#[derive(Debug, Clone, PartialEq)]
2651pub enum GetFocusedPaneInfoResponse {
2652    Ok { tab_index: usize, pane_id: PaneId },
2653    Err(String),
2654}
2655
2656#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2657pub enum SaveLayoutResponse {
2658    Ok(()),
2659    Err(String),
2660}
2661
2662#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2663pub enum DeleteLayoutResponse {
2664    Ok(()),
2665    Err(String),
2666}
2667
2668#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2669pub enum RenameLayoutResponse {
2670    Ok(()),
2671    Err(String),
2672}
2673
2674#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2675pub enum EditLayoutResponse {
2676    Ok(()),
2677    Err(String),
2678}
2679
2680#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2681pub struct SelectedText {
2682    pub start: Position,
2683    pub end: Position,
2684}
2685
2686impl SelectedText {
2687    pub fn new(start: Position, end: Position) -> Self {
2688        // Normalize: ensure start <= end
2689        let (normalized_start, normalized_end) = if start <= end {
2690            (start, end)
2691        } else {
2692            (end, start)
2693        };
2694
2695        // Normalize negative line values to 0
2696        // (column is already usize so can't be negative)
2697        let normalized_start = Position::new(
2698            normalized_start.line().max(0) as i32,
2699            normalized_start.column() as u16,
2700        );
2701        let normalized_end = Position::new(
2702            normalized_end.line().max(0) as i32,
2703            normalized_end.column() as u16,
2704        );
2705
2706        SelectedText {
2707            start: normalized_start,
2708            end: normalized_end,
2709        }
2710    }
2711
2712    pub fn from_positions(start: Position, end: Position) -> Option<Self> {
2713        if start == end {
2714            None
2715        } else {
2716            Some(Self::new(start, end))
2717        }
2718    }
2719}
2720
2721#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
2722pub struct PluginIds {
2723    pub plugin_id: u32,
2724    pub zellij_pid: u32,
2725    pub initial_cwd: PathBuf,
2726    pub client_id: ClientId,
2727}
2728
2729/// Tag used to identify the plugin in layout and config kdl files
2730#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
2731pub struct PluginTag(String);
2732
2733impl PluginTag {
2734    pub fn new(url: impl Into<String>) -> Self {
2735        PluginTag(url.into())
2736    }
2737}
2738
2739impl From<PluginTag> for String {
2740    fn from(tag: PluginTag) -> Self {
2741        tag.0
2742    }
2743}
2744
2745impl fmt::Display for PluginTag {
2746    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2747        write!(f, "{}", self.0)
2748    }
2749}
2750
2751#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
2752pub struct PluginCapabilities {
2753    pub arrow_fonts: bool,
2754}
2755
2756impl Default for PluginCapabilities {
2757    fn default() -> PluginCapabilities {
2758        PluginCapabilities { arrow_fonts: true }
2759    }
2760}
2761
2762/// Represents a Clipboard type
2763#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
2764pub enum CopyDestination {
2765    Command,
2766    Primary,
2767    System,
2768}
2769
2770#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
2771pub enum PermissionStatus {
2772    Granted,
2773    Denied,
2774}
2775
2776#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
2777pub struct FileToOpen {
2778    pub path: PathBuf,
2779    pub line_number: Option<usize>,
2780    pub cwd: Option<PathBuf>,
2781}
2782
2783impl FileToOpen {
2784    pub fn new<P: AsRef<Path>>(path: P) -> Self {
2785        FileToOpen {
2786            path: path.as_ref().to_path_buf(),
2787            ..Default::default()
2788        }
2789    }
2790    pub fn with_line_number(mut self, line_number: usize) -> Self {
2791        self.line_number = Some(line_number);
2792        self
2793    }
2794    pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
2795        self.cwd = Some(cwd);
2796        self
2797    }
2798}
2799
2800#[derive(Debug, Default, Clone)]
2801pub struct CommandToRun {
2802    pub path: PathBuf,
2803    pub args: Vec<String>,
2804    pub cwd: Option<PathBuf>,
2805}
2806
2807impl CommandToRun {
2808    pub fn new<P: AsRef<Path>>(path: P) -> Self {
2809        CommandToRun {
2810            path: path.as_ref().to_path_buf(),
2811            ..Default::default()
2812        }
2813    }
2814    pub fn new_with_args<P: AsRef<Path>, A: AsRef<str>>(path: P, args: Vec<A>) -> Self {
2815        CommandToRun {
2816            path: path.as_ref().to_path_buf(),
2817            args: args.into_iter().map(|a| a.as_ref().to_owned()).collect(),
2818            ..Default::default()
2819        }
2820    }
2821}
2822
2823#[derive(Debug, Default, Clone)]
2824pub struct MessageToPlugin {
2825    pub plugin_url: Option<String>,
2826    pub destination_plugin_id: Option<u32>,
2827    pub plugin_config: BTreeMap<String, String>,
2828    pub message_name: String,
2829    pub message_payload: Option<String>,
2830    pub message_args: BTreeMap<String, String>,
2831    /// these will only be used in case we need to launch a new plugin to send this message to,
2832    /// since none are running
2833    pub new_plugin_args: Option<NewPluginArgs>,
2834    pub floating_pane_coordinates: Option<FloatingPaneCoordinates>,
2835}
2836
2837#[derive(Debug, Default, Clone)]
2838pub struct NewPluginArgs {
2839    pub should_float: Option<bool>,
2840    pub pane_id_to_replace: Option<PaneId>,
2841    pub pane_title: Option<String>,
2842    pub cwd: Option<PathBuf>,
2843    pub skip_cache: bool,
2844    pub should_focus: Option<bool>,
2845}
2846
2847#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
2848pub enum PaneId {
2849    Terminal(u32),
2850    Plugin(u32),
2851}
2852
2853impl Default for PaneId {
2854    fn default() -> Self {
2855        PaneId::Terminal(0)
2856    }
2857}
2858
2859impl FromStr for PaneId {
2860    type Err = Box<dyn std::error::Error>;
2861    fn from_str(stringified_pane_id: &str) -> Result<Self, Self::Err> {
2862        if let Some(terminal_stringified_pane_id) = stringified_pane_id.strip_prefix("terminal_") {
2863            u32::from_str_radix(terminal_stringified_pane_id, 10)
2864                .map(|id| PaneId::Terminal(id))
2865                .map_err(|e| e.into())
2866        } else if let Some(plugin_pane_id) = stringified_pane_id.strip_prefix("plugin_") {
2867            u32::from_str_radix(plugin_pane_id, 10)
2868                .map(|id| PaneId::Plugin(id))
2869                .map_err(|e| e.into())
2870        } else {
2871            u32::from_str_radix(&stringified_pane_id, 10)
2872                .map(|id| PaneId::Terminal(id))
2873                .map_err(|e| e.into())
2874        }
2875    }
2876}
2877
2878impl std::fmt::Display for PaneId {
2879    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2880        match self {
2881            PaneId::Terminal(id) => write!(f, "terminal_{}", id),
2882            PaneId::Plugin(id) => write!(f, "plugin_{}", id),
2883        }
2884    }
2885}
2886
2887impl MessageToPlugin {
2888    pub fn new(message_name: impl Into<String>) -> Self {
2889        MessageToPlugin {
2890            message_name: message_name.into(),
2891            ..Default::default()
2892        }
2893    }
2894    pub fn with_plugin_url(mut self, url: impl Into<String>) -> Self {
2895        self.plugin_url = Some(url.into());
2896        self
2897    }
2898    pub fn with_destination_plugin_id(mut self, destination_plugin_id: u32) -> Self {
2899        self.destination_plugin_id = Some(destination_plugin_id);
2900        self
2901    }
2902    pub fn with_plugin_config(mut self, plugin_config: BTreeMap<String, String>) -> Self {
2903        self.plugin_config = plugin_config;
2904        self
2905    }
2906    pub fn with_payload(mut self, payload: impl Into<String>) -> Self {
2907        self.message_payload = Some(payload.into());
2908        self
2909    }
2910    pub fn with_args(mut self, args: BTreeMap<String, String>) -> Self {
2911        self.message_args = args;
2912        self
2913    }
2914    pub fn with_floating_pane_coordinates(
2915        mut self,
2916        floating_pane_coordinates: FloatingPaneCoordinates,
2917    ) -> Self {
2918        self.floating_pane_coordinates = Some(floating_pane_coordinates);
2919        self
2920    }
2921    pub fn new_plugin_instance_should_float(mut self, should_float: bool) -> Self {
2922        let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
2923        new_plugin_args.should_float = Some(should_float);
2924        self
2925    }
2926    pub fn new_plugin_instance_should_replace_pane(mut self, pane_id: PaneId) -> Self {
2927        let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
2928        new_plugin_args.pane_id_to_replace = Some(pane_id);
2929        self
2930    }
2931    pub fn new_plugin_instance_should_have_pane_title(
2932        mut self,
2933        pane_title: impl Into<String>,
2934    ) -> Self {
2935        let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
2936        new_plugin_args.pane_title = Some(pane_title.into());
2937        self
2938    }
2939    pub fn new_plugin_instance_should_have_cwd(mut self, cwd: PathBuf) -> Self {
2940        let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
2941        new_plugin_args.cwd = Some(cwd);
2942        self
2943    }
2944    pub fn new_plugin_instance_should_skip_cache(mut self) -> Self {
2945        let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
2946        new_plugin_args.skip_cache = true;
2947        self
2948    }
2949    pub fn new_plugin_instance_should_be_focused(mut self) -> Self {
2950        let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
2951        new_plugin_args.should_focus = Some(true);
2952        self
2953    }
2954    pub fn has_cwd(&self) -> bool {
2955        self.new_plugin_args
2956            .as_ref()
2957            .map(|n| n.cwd.is_some())
2958            .unwrap_or(false)
2959    }
2960}
2961
2962#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
2963pub struct ConnectToSession {
2964    pub name: Option<String>,
2965    pub tab_position: Option<usize>,
2966    pub pane_id: Option<(u32, bool)>, // (id, is_plugin)
2967    pub layout: Option<LayoutInfo>,
2968    pub cwd: Option<PathBuf>,
2969}
2970
2971impl ConnectToSession {
2972    pub fn apply_layout_dir(&mut self, layout_dir: &PathBuf) {
2973        if let Some(LayoutInfo::File(file_path, _layout_metadata)) = self.layout.as_mut() {
2974            *file_path = Path::join(layout_dir, &file_path)
2975                .to_string_lossy()
2976                .to_string();
2977        }
2978    }
2979}
2980
2981#[derive(Debug, Default, Clone)]
2982pub struct PluginMessage {
2983    pub name: String,
2984    pub payload: String,
2985    pub worker_name: Option<String>,
2986}
2987
2988impl PluginMessage {
2989    pub fn new_to_worker(worker_name: &str, message: &str, payload: &str) -> Self {
2990        PluginMessage {
2991            name: message.to_owned(),
2992            payload: payload.to_owned(),
2993            worker_name: Some(worker_name.to_owned()),
2994        }
2995    }
2996    pub fn new_to_plugin(message: &str, payload: &str) -> Self {
2997        PluginMessage {
2998            name: message.to_owned(),
2999            payload: payload.to_owned(),
3000            worker_name: None,
3001        }
3002    }
3003}
3004
3005#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3006pub enum HttpVerb {
3007    Get,
3008    Post,
3009    Put,
3010    Delete,
3011}
3012
3013#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3014pub enum PipeSource {
3015    Cli(String), // String is the pipe_id of the CLI pipe (used for blocking/unblocking)
3016    Plugin(u32), // u32 is the lugin id
3017    Keybind,     // TODO: consider including the actual keybind here?
3018}
3019
3020#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3021pub struct PipeMessage {
3022    pub source: PipeSource,
3023    pub name: String,
3024    pub payload: Option<String>,
3025    pub args: BTreeMap<String, String>,
3026    pub is_private: bool,
3027}
3028
3029impl PipeMessage {
3030    pub fn new(
3031        source: PipeSource,
3032        name: impl Into<String>,
3033        payload: &Option<String>,
3034        args: &Option<BTreeMap<String, String>>,
3035        is_private: bool,
3036    ) -> Self {
3037        PipeMessage {
3038            source,
3039            name: name.into(),
3040            payload: payload.clone(),
3041            args: args.clone().unwrap_or_else(|| Default::default()),
3042            is_private,
3043        }
3044    }
3045}
3046
3047#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
3048pub struct FloatingPaneCoordinates {
3049    pub x: Option<PercentOrFixed>,
3050    pub y: Option<PercentOrFixed>,
3051    pub width: Option<PercentOrFixed>,
3052    pub height: Option<PercentOrFixed>,
3053    pub pinned: Option<bool>,
3054    pub borderless: Option<bool>,
3055}
3056
3057impl FloatingPaneCoordinates {
3058    pub fn new(
3059        x: Option<String>,
3060        y: Option<String>,
3061        width: Option<String>,
3062        height: Option<String>,
3063        pinned: Option<bool>,
3064        borderless: Option<bool>,
3065    ) -> Option<Self> {
3066        // Parse x/y coordinates - allows 0% or 0
3067        let x = x.and_then(|x| PercentOrFixed::from_str(&x).ok());
3068        let y = y.and_then(|y| PercentOrFixed::from_str(&y).ok());
3069
3070        // Parse width/height - reject 0% or 0
3071        let width = width.and_then(|w| {
3072            PercentOrFixed::from_str(&w)
3073                .ok()
3074                .and_then(|size| match size {
3075                    PercentOrFixed::Percent(0) => None,
3076                    PercentOrFixed::Fixed(0) => None,
3077                    _ => Some(size),
3078                })
3079        });
3080        let height = height.and_then(|h| {
3081            PercentOrFixed::from_str(&h)
3082                .ok()
3083                .and_then(|size| match size {
3084                    PercentOrFixed::Percent(0) => None,
3085                    PercentOrFixed::Fixed(0) => None,
3086                    _ => Some(size),
3087                })
3088        });
3089
3090        if x.is_none()
3091            && y.is_none()
3092            && width.is_none()
3093            && height.is_none()
3094            && pinned.is_none()
3095            && borderless.is_none()
3096        {
3097            None
3098        } else {
3099            Some(FloatingPaneCoordinates {
3100                x,
3101                y,
3102                width,
3103                height,
3104                pinned,
3105                borderless,
3106            })
3107        }
3108    }
3109    pub fn with_x_fixed(mut self, x: usize) -> Self {
3110        self.x = Some(PercentOrFixed::Fixed(x));
3111        self
3112    }
3113    pub fn with_x_percent(mut self, x: usize) -> Self {
3114        if x > 100 {
3115            eprintln!("x must be between 0 and 100");
3116            return self;
3117        }
3118        self.x = Some(PercentOrFixed::Percent(x));
3119        self
3120    }
3121    pub fn with_y_fixed(mut self, y: usize) -> Self {
3122        self.y = Some(PercentOrFixed::Fixed(y));
3123        self
3124    }
3125    pub fn with_y_percent(mut self, y: usize) -> Self {
3126        if y > 100 {
3127            eprintln!("y must be between 0 and 100");
3128            return self;
3129        }
3130        self.y = Some(PercentOrFixed::Percent(y));
3131        self
3132    }
3133    pub fn with_width_fixed(mut self, width: usize) -> Self {
3134        self.width = Some(PercentOrFixed::Fixed(width));
3135        self
3136    }
3137    pub fn with_width_percent(mut self, width: usize) -> Self {
3138        if width > 100 {
3139            eprintln!("width must be between 0 and 100");
3140            return self;
3141        }
3142        self.width = Some(PercentOrFixed::Percent(width));
3143        self
3144    }
3145    pub fn with_height_fixed(mut self, height: usize) -> Self {
3146        self.height = Some(PercentOrFixed::Fixed(height));
3147        self
3148    }
3149    pub fn with_height_percent(mut self, height: usize) -> Self {
3150        if height > 100 {
3151            eprintln!("height must be between 0 and 100");
3152            return self;
3153        }
3154        self.height = Some(PercentOrFixed::Percent(height));
3155        self
3156    }
3157}
3158
3159impl From<PaneGeom> for FloatingPaneCoordinates {
3160    fn from(pane_geom: PaneGeom) -> Self {
3161        FloatingPaneCoordinates {
3162            x: Some(PercentOrFixed::Fixed(pane_geom.x)),
3163            y: Some(PercentOrFixed::Fixed(pane_geom.y)),
3164            width: Some(PercentOrFixed::Fixed(pane_geom.cols.as_usize())),
3165            height: Some(PercentOrFixed::Fixed(pane_geom.rows.as_usize())),
3166            pinned: Some(pane_geom.is_pinned),
3167            borderless: None,
3168        }
3169    }
3170}
3171
3172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3173pub struct OriginatingPlugin {
3174    pub plugin_id: u32,
3175    pub client_id: ClientId,
3176    pub context: Context,
3177}
3178
3179impl OriginatingPlugin {
3180    pub fn new(plugin_id: u32, client_id: ClientId, context: Context) -> Self {
3181        OriginatingPlugin {
3182            plugin_id,
3183            client_id,
3184            context,
3185        }
3186    }
3187}
3188
3189#[derive(ValueEnum, Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
3190pub enum WebSharing {
3191    #[serde(alias = "on")]
3192    On,
3193    #[serde(alias = "off")]
3194    Off,
3195    #[serde(alias = "disabled")]
3196    Disabled,
3197}
3198
3199impl Default for WebSharing {
3200    fn default() -> Self {
3201        Self::Off
3202    }
3203}
3204
3205impl WebSharing {
3206    pub fn is_on(&self) -> bool {
3207        match self {
3208            WebSharing::On => true,
3209            _ => false,
3210        }
3211    }
3212    pub fn web_clients_allowed(&self) -> bool {
3213        match self {
3214            WebSharing::On => true,
3215            _ => false,
3216        }
3217    }
3218    pub fn sharing_is_disabled(&self) -> bool {
3219        match self {
3220            WebSharing::Disabled => true,
3221            _ => false,
3222        }
3223    }
3224    pub fn set_sharing(&mut self) -> bool {
3225        // returns true if successfully set sharing
3226        match self {
3227            WebSharing::On => true,
3228            WebSharing::Off => {
3229                *self = WebSharing::On;
3230                true
3231            },
3232            WebSharing::Disabled => false,
3233        }
3234    }
3235    pub fn set_not_sharing(&mut self) -> bool {
3236        // returns true if successfully set not sharing
3237        match self {
3238            WebSharing::On => {
3239                *self = WebSharing::Off;
3240                true
3241            },
3242            WebSharing::Off => true,
3243            WebSharing::Disabled => false,
3244        }
3245    }
3246}
3247
3248impl FromStr for WebSharing {
3249    type Err = String;
3250    fn from_str(s: &str) -> Result<Self, Self::Err> {
3251        match s {
3252            "On" | "on" => Ok(Self::On),
3253            "Off" | "off" => Ok(Self::Off),
3254            "Disabled" | "disabled" => Ok(Self::Disabled),
3255            _ => Err(format!("No such option: {}", s)),
3256        }
3257    }
3258}
3259
3260#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
3261pub enum NewPanePlacement {
3262    NoPreference {
3263        borderless: Option<bool>,
3264    },
3265    Tiled {
3266        direction: Option<Direction>,
3267        borderless: Option<bool>,
3268    },
3269    Floating(Option<FloatingPaneCoordinates>),
3270    InPlace {
3271        pane_id_to_replace: Option<PaneId>,
3272        close_replaced_pane: bool,
3273        borderless: Option<bool>,
3274    },
3275    Stacked {
3276        pane_id_to_stack_under: Option<PaneId>,
3277        borderless: Option<bool>,
3278    },
3279}
3280
3281impl Default for NewPanePlacement {
3282    fn default() -> Self {
3283        NewPanePlacement::NoPreference { borderless: None }
3284    }
3285}
3286
3287impl NewPanePlacement {
3288    pub fn with_floating_pane_coordinates(
3289        floating_pane_coordinates: Option<FloatingPaneCoordinates>,
3290    ) -> Self {
3291        NewPanePlacement::Floating(floating_pane_coordinates)
3292    }
3293    pub fn with_should_be_in_place(
3294        self,
3295        should_be_in_place: bool,
3296        close_replaced_pane: bool,
3297    ) -> Self {
3298        if should_be_in_place {
3299            NewPanePlacement::InPlace {
3300                pane_id_to_replace: None,
3301                close_replaced_pane,
3302                borderless: None,
3303            }
3304        } else {
3305            self
3306        }
3307    }
3308    pub fn with_pane_id_to_replace(
3309        pane_id_to_replace: Option<PaneId>,
3310        close_replaced_pane: bool,
3311    ) -> Self {
3312        NewPanePlacement::InPlace {
3313            pane_id_to_replace,
3314            close_replaced_pane,
3315            borderless: None,
3316        }
3317    }
3318    pub fn should_float(&self) -> Option<bool> {
3319        match self {
3320            NewPanePlacement::Floating(_) => Some(true),
3321            NewPanePlacement::Tiled { .. } => Some(false),
3322            _ => None,
3323        }
3324    }
3325    pub fn floating_pane_coordinates(&self) -> Option<FloatingPaneCoordinates> {
3326        match self {
3327            NewPanePlacement::Floating(floating_pane_coordinates) => {
3328                floating_pane_coordinates.clone()
3329            },
3330            _ => None,
3331        }
3332    }
3333    pub fn should_stack(&self) -> bool {
3334        match self {
3335            NewPanePlacement::Stacked { .. } => true,
3336            _ => false,
3337        }
3338    }
3339    pub fn id_of_stack_root(&self) -> Option<PaneId> {
3340        match self {
3341            NewPanePlacement::Stacked {
3342                pane_id_to_stack_under,
3343                ..
3344            } => *pane_id_to_stack_under,
3345            _ => None,
3346        }
3347    }
3348    pub fn get_borderless(&self) -> Option<bool> {
3349        match self {
3350            NewPanePlacement::NoPreference { borderless } => *borderless,
3351            NewPanePlacement::Tiled { borderless, .. } => *borderless,
3352            NewPanePlacement::Floating(coords) => coords.as_ref().and_then(|c| c.borderless),
3353            NewPanePlacement::InPlace { borderless, .. } => *borderless,
3354            NewPanePlacement::Stacked { borderless, .. } => *borderless,
3355        }
3356    }
3357}
3358
3359type Context = BTreeMap<String, String>;
3360
3361#[derive(Debug, Clone, EnumDiscriminants, Display)]
3362#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))]
3363#[strum_discriminants(name(CommandType))]
3364pub enum PluginCommand {
3365    Subscribe(HashSet<EventType>),
3366    Unsubscribe(HashSet<EventType>),
3367    SetSelectable(bool),
3368    ShowCursor(Option<(usize, usize)>),
3369    GetPluginIds,
3370    GetZellijVersion,
3371    OpenFile(FileToOpen, Context),
3372    OpenFileFloating(FileToOpen, Option<FloatingPaneCoordinates>, Context),
3373    OpenTerminal(FileToOpen), // only used for the path as cwd
3374    OpenTerminalFloating(FileToOpen, Option<FloatingPaneCoordinates>), // only used for the path as cwd
3375    OpenCommandPane(CommandToRun, Context),
3376    OpenCommandPaneFloating(CommandToRun, Option<FloatingPaneCoordinates>, Context),
3377    SwitchTabTo(u32), // tab index
3378    SetTimeout(f64),  // seconds
3379    ExecCmd(Vec<String>),
3380    PostMessageTo(PluginMessage),
3381    PostMessageToPlugin(PluginMessage),
3382    HideSelf,
3383    ShowSelf(bool), // bool - should float if hidden
3384    SwitchToMode(InputMode),
3385    NewTabsWithLayout(String), // raw kdl layout
3386    NewTab {
3387        name: Option<String>,
3388        cwd: Option<String>,
3389    },
3390    NewTabUnfocused {
3391        name: Option<String>,
3392        cwd: Option<String>,
3393    },
3394    NewTiledPaneInTab {
3395        tab_position: usize,
3396    },
3397    ToggleFloatingPanes {
3398        tab_id: Option<u64>,
3399    },
3400    NewPane,
3401    GoToNextTab,
3402    GoToPreviousTab,
3403    Resize(Resize),
3404    ResizeWithDirection(ResizeStrategy),
3405    FocusNextPane,
3406    FocusPreviousPane,
3407    FocusLastPane,
3408    MoveFocus(Direction),
3409    MoveFocusOrTab(Direction),
3410    Detach,
3411    EditScrollback,
3412    Write(Vec<u8>), // bytes
3413    WriteChars(String),
3414    ToggleTab,
3415    MovePane,
3416    MovePaneWithDirection(Direction),
3417    ClearScreen,
3418    ScrollUp,
3419    ScrollDown,
3420    ScrollToTop,
3421    ScrollToBottom,
3422    PageScrollUp,
3423    PageScrollDown,
3424    ToggleFocusFullscreen,
3425    ToggleFocusNoUiFullscreen,
3426    TogglePaneFrames,
3427    SetPaneFrameStyle(PaneFrameStyle),
3428    TogglePaneEmbedOrEject,
3429    UndoRenamePane,
3430    CloseFocus,
3431    ToggleActiveTabSync,
3432    CloseFocusedTab,
3433    UndoRenameTab,
3434    QuitZellij,
3435    PreviousSwapLayout,
3436    NextSwapLayout,
3437    GoToTabName(String),
3438    FocusOrCreateTab(String),
3439    GoToTab(u32),                       // tab index
3440    StartOrReloadPlugin(String),        // plugin url (eg. file:/path/to/plugin.wasm)
3441    CloseTerminalPane(u32),             // terminal pane id
3442    ClosePluginPane(u32),               // plugin pane id
3443    FocusTerminalPane(u32, bool, bool), // terminal pane id, should_float_if_hidden, should_be_in_place_if_hidden
3444    FocusPluginPane(u32, bool, bool), // plugin pane id, should_float_if_hidden, should_be_in_place_if_hidden
3445    RenameTerminalPane(u32, String),  // terminal pane id, new name
3446    RenamePluginPane(u32, String),    // plugin pane id, new name
3447    RenameTab(u32, String),           // tab index, new name
3448    ReportPanic(String),              // stringified panic
3449    RequestPluginPermissions(Vec<PermissionType>),
3450    SwitchSession(ConnectToSession),
3451    DeleteDeadSession(String),       // String -> session name
3452    DeleteAllDeadSessions,           // String -> session name
3453    OpenTerminalInPlace(FileToOpen), // only used for the path as cwd
3454    OpenFileInPlace(FileToOpen, Context),
3455    OpenCommandPaneInPlace(CommandToRun, Context),
3456    RunCommand(
3457        Vec<String>,              // command
3458        BTreeMap<String, String>, // env_variables
3459        PathBuf,                  // cwd
3460        BTreeMap<String, String>, // context
3461    ),
3462    WebRequest(
3463        String, // url
3464        HttpVerb,
3465        BTreeMap<String, String>, // headers
3466        Vec<u8>,                  // body
3467        BTreeMap<String, String>, // context
3468    ),
3469    RenameSession(String),         // String -> new session name
3470    UnblockCliPipeInput(String),   // String => pipe name
3471    BlockCliPipeInput(String),     // String => pipe name
3472    CliPipeOutput(String, String), // String => pipe name, String => output
3473    MessageToPlugin(MessageToPlugin),
3474    DisconnectOtherClients,
3475    KillSessions(Vec<String>), // one or more session names
3476    ScanHostFolder(PathBuf),   // TODO: rename to ScanHostFolder
3477    WatchFilesystem,
3478    DumpSessionLayout {
3479        tab_index: Option<usize>,
3480    },
3481    CloseSelf,
3482    NewTabsWithLayoutInfo(LayoutInfo),
3483    Reconfigure(String, bool), // String -> stringified configuration, bool -> save configuration
3484    // file to disk
3485    HidePaneWithId(PaneId),
3486    ShowPaneWithId(PaneId, bool, bool), // bools -> should_float_if_hidden, should_focus_pane
3487    OpenCommandPaneBackground(CommandToRun, Context),
3488    RerunCommandPane(u32), // u32  - terminal pane id
3489    ResizePaneIdWithDirection(ResizeStrategy, PaneId),
3490    EditScrollbackForPaneWithId(PaneId),
3491    GetPaneScrollback {
3492        pane_id: PaneId,
3493        get_full_scrollback: bool,
3494    },
3495    WriteToPaneId(Vec<u8>, PaneId),
3496    WriteCharsToPaneId(String, PaneId),
3497    SendSigintToPaneId(PaneId),
3498    SendSigkillToPaneId(PaneId),
3499    GetPanePid {
3500        pane_id: PaneId,
3501    },
3502    GetPaneRunningCommand {
3503        pane_id: PaneId,
3504    },
3505    GetPaneCwd {
3506        pane_id: PaneId,
3507    },
3508    MovePaneWithPaneId(PaneId),
3509    MovePaneWithPaneIdInDirection(PaneId, Direction),
3510    ClearScreenForPaneId(PaneId),
3511    ScrollUpInPaneId(PaneId),
3512    ScrollDownInPaneId(PaneId),
3513    ScrollToTopInPaneId(PaneId),
3514    ScrollToBottomInPaneId(PaneId),
3515    PageScrollUpInPaneId(PaneId),
3516    PageScrollDownInPaneId(PaneId),
3517    TogglePaneIdFullscreen(PaneId),
3518    TogglePaneEmbedOrEjectForPaneId(PaneId),
3519    CloseTabWithIndex(usize), // usize - tab_index
3520    BreakPanesToNewTab(Vec<PaneId>, Option<String>, bool), // bool -
3521    // should_change_focus_to_new_tab,
3522    // Option<String> - optional name for
3523    // the new tab
3524    BreakPanesToTabWithIndex(Vec<PaneId>, usize, bool), // usize - tab_index, bool -
3525    // should_change_focus_to_new_tab
3526    SwitchTabToId(u64),                            // u64 - tab_id
3527    GoToTabWithId(u64),                            // u64 - tab_id
3528    CloseTabWithId(u64),                           // u64 - tab_id
3529    RenameTabWithId(u64, String),                  // u64 - tab_id, String - new name
3530    BreakPanesToTabWithId(Vec<PaneId>, u64, bool), // u64 - tab_id, bool -
3531    // should_change_focus_to_target_tab
3532    ReloadPlugin(u32), // u32 - plugin pane id
3533    LoadNewPlugin {
3534        url: String,
3535        config: BTreeMap<String, String>,
3536        load_in_background: bool,
3537        skip_plugin_cache: bool,
3538    },
3539    RebindKeys {
3540        keys_to_rebind: Vec<(InputMode, KeyWithModifier, Vec<Action>)>,
3541        keys_to_unbind: Vec<(InputMode, KeyWithModifier)>,
3542        write_config_to_disk: bool,
3543    },
3544    ListClients,
3545    ChangeHostFolder(PathBuf),
3546    SetFloatingPanePinned(PaneId, bool), // bool -> should be pinned
3547    StackPanes(Vec<PaneId>),
3548    ChangeFloatingPanesCoordinates(Vec<(PaneId, FloatingPaneCoordinates)>),
3549    TogglePaneBorderless(PaneId),
3550    SetPaneBorderless(PaneId, bool),
3551    OpenCommandPaneNearPlugin(CommandToRun, Context),
3552    OpenTerminalNearPlugin(FileToOpen),
3553    OpenTerminalFloatingNearPlugin(FileToOpen, Option<FloatingPaneCoordinates>),
3554    OpenTerminalInPlaceOfPlugin(FileToOpen, bool), // bool -> close_plugin_after_replace
3555    OpenCommandPaneFloatingNearPlugin(CommandToRun, Option<FloatingPaneCoordinates>, Context),
3556    OpenCommandPaneInPlaceOfPlugin(CommandToRun, bool, Context), // bool ->
3557    // close_plugin_after_replace
3558    OpenFileNearPlugin(FileToOpen, Context),
3559    OpenFileFloatingNearPlugin(FileToOpen, Option<FloatingPaneCoordinates>, Context),
3560    StartWebServer,
3561    StopWebServer,
3562    ShareCurrentSession,
3563    StopSharingCurrentSession,
3564    OpenFileInPlaceOfPlugin(FileToOpen, bool, Context), // bool -> close_plugin_after_replace
3565    GroupAndUngroupPanes(Vec<PaneId>, Vec<PaneId>, bool), // panes to group, panes to ungroup,
3566    // bool -> for all clients
3567    HighlightAndUnhighlightPanes(Vec<PaneId>, Vec<PaneId>), // panes to highlight, panes to
3568    // unhighlight
3569    CloseMultiplePanes(Vec<PaneId>),
3570    FloatMultiplePanes(Vec<PaneId>),
3571    EmbedMultiplePanes(Vec<PaneId>),
3572    QueryWebServerStatus,
3573    SetSelfMouseSelectionSupport(bool),
3574    GenerateWebLoginToken(Option<String>, bool), // (token_label, read_only)
3575    RevokeWebLoginToken(String), // String -> token id (provided name or generated id)
3576    ListWebLoginTokens,
3577    RevokeAllWebLoginTokens,
3578    RenameWebLoginToken(String, String), // (original_name, new_name)
3579    InterceptKeyPresses,
3580    ClearKeyPressesIntercepts,
3581    ReplacePaneWithExistingPane(PaneId, PaneId, bool), // (pane id to replace, pane id of existing,
3582    // suppress_replaced_pane)
3583    RunAction(Action, BTreeMap<String, String>),
3584    CopyToClipboard(String), // text to copy
3585    OverrideLayout(
3586        LayoutInfo,
3587        bool,                     // retain_existing_terminal_panes
3588        bool,                     // retain_existing_plugin_panes
3589        bool,                     // apply_only_to_active_tab,
3590        BTreeMap<String, String>, // context
3591    ),
3592    SaveLayout {
3593        layout_name: String,
3594        layout_kdl: String,
3595        overwrite: bool,
3596    },
3597    DeleteLayout {
3598        layout_name: String,
3599    },
3600    RenameLayout {
3601        old_layout_name: String,
3602        new_layout_name: String,
3603    },
3604    EditLayout {
3605        layout_name: String,
3606        context: Context,
3607    },
3608    GenerateRandomName,
3609    DumpLayout(String),
3610    ParseLayout(String), // String contains raw KDL layout
3611    GetLayoutDir,
3612    GetFocusedPaneInfo,
3613    SaveSession,
3614    CurrentSessionLastSavedTime,
3615    GetPaneInfo(PaneId),
3616    GetTabInfo(usize), // tab_id
3617    GetSessionEnvironmentVariables,
3618    OpenCommandPaneInNewTab(CommandToRun, Context),
3619    OpenPluginPaneInNewTab {
3620        plugin_url: String,
3621        configuration: BTreeMap<String, String>,
3622        context: Context,
3623    },
3624    OpenEditorPaneInNewTab(FileToOpen, Context),
3625    OpenCommandPaneInPlaceOfPaneId(PaneId, CommandToRun, bool, Context), // bool = close_replaced_pane
3626    OpenTerminalPaneInPlaceOfPaneId(PaneId, FileToOpen, bool),
3627    OpenEditPaneInPlaceOfPaneId(PaneId, FileToOpen, bool, Context),
3628    HideFloatingPanes {
3629        tab_id: Option<usize>,
3630    },
3631    ShowFloatingPanes {
3632        tab_id: Option<usize>,
3633    },
3634    SetPaneColor(PaneId, Option<String>, Option<String>), // (pane_id, fg, bg)
3635    SetPaneRegexHighlights(PaneId, Vec<RegexHighlight>),
3636    ClearPaneHighlights(PaneId),
3637    OpenPluginPaneFloating {
3638        plugin_url: String,
3639        configuration: BTreeMap<String, String>,
3640        floating_pane_coordinates: Option<FloatingPaneCoordinates>,
3641        context: BTreeMap<String, String>,
3642    },
3643    ListWindowsVolumes,
3644    GetSessionList,
3645    KillSessionsAndReply(Vec<String>), // one or more session names; sends a response back
3646    DeleteDeadSessionAndReply(String), // session name; sends a response back
3647    DeleteAllDeadSessionsAndReply,     // no payload; sends a response back
3648    SetSoftKeyboard(bool),
3649    FocusHostSession,
3650}
3651
3652// Response type for plugin API methods that open a pane in a new tab
3653#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3654pub struct OpenPaneInNewTabResponse {
3655    pub tab_id: Option<usize>,
3656    pub pane_id: Option<PaneId>,
3657}
3658
3659// Response types for plugin API methods that create tabs
3660pub type NewTabResponse = Option<usize>;
3661pub type NewTabUnfocusedResponse = Option<usize>;
3662pub type NewTabsResponse = Vec<usize>;
3663pub type FocusOrCreateTabResponse = Option<usize>;
3664pub type BreakPanesToNewTabResponse = Option<usize>;
3665pub type BreakPanesToTabWithIndexResponse = Option<usize>;
3666pub type BreakPanesToTabWithIdResponse = Option<usize>;
3667
3668// Response types for plugin API methods that create panes
3669pub type OpenFileResponse = Option<PaneId>;
3670pub type OpenFileFloatingResponse = Option<PaneId>;
3671pub type OpenFileInPlaceResponse = Option<PaneId>;
3672pub type OpenFileNearPluginResponse = Option<PaneId>;
3673pub type OpenFileFloatingNearPluginResponse = Option<PaneId>;
3674pub type OpenFileInPlaceOfPluginResponse = Option<PaneId>;
3675
3676pub type OpenTerminalResponse = Option<PaneId>;
3677pub type OpenTerminalFloatingResponse = Option<PaneId>;
3678pub type OpenTerminalInPlaceResponse = Option<PaneId>;
3679pub type OpenTerminalNearPluginResponse = Option<PaneId>;
3680pub type OpenTerminalFloatingNearPluginResponse = Option<PaneId>;
3681pub type OpenTerminalInPlaceOfPluginResponse = Option<PaneId>;
3682pub type NewTiledPaneInTabResponse = Option<PaneId>;
3683
3684pub type OpenCommandPaneResponse = Option<PaneId>;
3685pub type OpenCommandPaneFloatingResponse = Option<PaneId>;
3686pub type OpenCommandPaneInPlaceResponse = Option<PaneId>;
3687pub type OpenCommandPaneNearPluginResponse = Option<PaneId>;
3688pub type OpenCommandPaneFloatingNearPluginResponse = Option<PaneId>;
3689pub type OpenCommandPaneInPlaceOfPluginResponse = Option<PaneId>;
3690pub type OpenCommandPaneBackgroundResponse = Option<PaneId>;
3691pub type OpenCommandPaneInPlaceOfPaneIdResponse = Option<PaneId>;
3692pub type OpenTerminalPaneInPlaceOfPaneIdResponse = Option<PaneId>;
3693pub type OpenEditPaneInPlaceOfPaneIdResponse = Option<PaneId>;
3694pub type OpenPluginPaneFloatingResponse = Option<PaneId>;
3695
3696#[test]
3697pub fn can_parse_unicode_bare_keys() {
3698    let key = "1087"; // п
3699    assert_eq!(
3700        BareKey::from_bytes_with_u(&key.as_bytes()),
3701        Some(BareKey::Char('п')),
3702        "Can parse a bare 'п' keypress"
3703    );
3704    let key = "1255"; // ӧ
3705    assert_eq!(
3706        BareKey::from_bytes_with_u(&key.as_bytes()),
3707        Some(BareKey::Char('ӧ')),
3708        "Can parse a bare 'ӧ' keypress"
3709    );
3710    let key = "1098"; // ъ
3711    assert_eq!(
3712        BareKey::from_bytes_with_u(&key.as_bytes()),
3713        Some(BareKey::Char('ъ')),
3714        "Can parse a bare 'ъ' keypress"
3715    );
3716}