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