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 KillSessionsResponse {
2607    Ok,
2608    Err(String),
2609}
2610
2611#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2612pub enum DeleteDeadSessionResponse {
2613    Ok,
2614    Err(String),
2615}
2616
2617#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2618pub enum DeleteAllDeadSessionsResponse {
2619    Ok,
2620    Err(String),
2621}
2622
2623#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2624pub enum GetPaneCwdResponse {
2625    Ok(PathBuf),
2626    Err(String),
2627}
2628
2629#[derive(Debug, Clone, PartialEq)]
2630pub enum GetFocusedPaneInfoResponse {
2631    Ok { tab_index: usize, pane_id: PaneId },
2632    Err(String),
2633}
2634
2635#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2636pub enum SaveLayoutResponse {
2637    Ok(()),
2638    Err(String),
2639}
2640
2641#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2642pub enum DeleteLayoutResponse {
2643    Ok(()),
2644    Err(String),
2645}
2646
2647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2648pub enum RenameLayoutResponse {
2649    Ok(()),
2650    Err(String),
2651}
2652
2653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2654pub enum EditLayoutResponse {
2655    Ok(()),
2656    Err(String),
2657}
2658
2659#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2660pub struct SelectedText {
2661    pub start: Position,
2662    pub end: Position,
2663}
2664
2665impl SelectedText {
2666    pub fn new(start: Position, end: Position) -> Self {
2667        // Normalize: ensure start <= end
2668        let (normalized_start, normalized_end) = if start <= end {
2669            (start, end)
2670        } else {
2671            (end, start)
2672        };
2673
2674        // Normalize negative line values to 0
2675        // (column is already usize so can't be negative)
2676        let normalized_start = Position::new(
2677            normalized_start.line().max(0) as i32,
2678            normalized_start.column() as u16,
2679        );
2680        let normalized_end = Position::new(
2681            normalized_end.line().max(0) as i32,
2682            normalized_end.column() as u16,
2683        );
2684
2685        SelectedText {
2686            start: normalized_start,
2687            end: normalized_end,
2688        }
2689    }
2690
2691    pub fn from_positions(start: Position, end: Position) -> Option<Self> {
2692        if start == end {
2693            None
2694        } else {
2695            Some(Self::new(start, end))
2696        }
2697    }
2698}
2699
2700#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
2701pub struct PluginIds {
2702    pub plugin_id: u32,
2703    pub zellij_pid: u32,
2704    pub initial_cwd: PathBuf,
2705    pub client_id: ClientId,
2706}
2707
2708/// Tag used to identify the plugin in layout and config kdl files
2709#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
2710pub struct PluginTag(String);
2711
2712impl PluginTag {
2713    pub fn new(url: impl Into<String>) -> Self {
2714        PluginTag(url.into())
2715    }
2716}
2717
2718impl From<PluginTag> for String {
2719    fn from(tag: PluginTag) -> Self {
2720        tag.0
2721    }
2722}
2723
2724impl fmt::Display for PluginTag {
2725    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2726        write!(f, "{}", self.0)
2727    }
2728}
2729
2730#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
2731pub struct PluginCapabilities {
2732    pub arrow_fonts: bool,
2733}
2734
2735impl Default for PluginCapabilities {
2736    fn default() -> PluginCapabilities {
2737        PluginCapabilities { arrow_fonts: true }
2738    }
2739}
2740
2741/// Represents a Clipboard type
2742#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
2743pub enum CopyDestination {
2744    Command,
2745    Primary,
2746    System,
2747}
2748
2749#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
2750pub enum PermissionStatus {
2751    Granted,
2752    Denied,
2753}
2754
2755#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
2756pub struct FileToOpen {
2757    pub path: PathBuf,
2758    pub line_number: Option<usize>,
2759    pub cwd: Option<PathBuf>,
2760}
2761
2762impl FileToOpen {
2763    pub fn new<P: AsRef<Path>>(path: P) -> Self {
2764        FileToOpen {
2765            path: path.as_ref().to_path_buf(),
2766            ..Default::default()
2767        }
2768    }
2769    pub fn with_line_number(mut self, line_number: usize) -> Self {
2770        self.line_number = Some(line_number);
2771        self
2772    }
2773    pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
2774        self.cwd = Some(cwd);
2775        self
2776    }
2777}
2778
2779#[derive(Debug, Default, Clone)]
2780pub struct CommandToRun {
2781    pub path: PathBuf,
2782    pub args: Vec<String>,
2783    pub cwd: Option<PathBuf>,
2784}
2785
2786impl CommandToRun {
2787    pub fn new<P: AsRef<Path>>(path: P) -> Self {
2788        CommandToRun {
2789            path: path.as_ref().to_path_buf(),
2790            ..Default::default()
2791        }
2792    }
2793    pub fn new_with_args<P: AsRef<Path>, A: AsRef<str>>(path: P, args: Vec<A>) -> Self {
2794        CommandToRun {
2795            path: path.as_ref().to_path_buf(),
2796            args: args.into_iter().map(|a| a.as_ref().to_owned()).collect(),
2797            ..Default::default()
2798        }
2799    }
2800}
2801
2802#[derive(Debug, Default, Clone)]
2803pub struct MessageToPlugin {
2804    pub plugin_url: Option<String>,
2805    pub destination_plugin_id: Option<u32>,
2806    pub plugin_config: BTreeMap<String, String>,
2807    pub message_name: String,
2808    pub message_payload: Option<String>,
2809    pub message_args: BTreeMap<String, String>,
2810    /// these will only be used in case we need to launch a new plugin to send this message to,
2811    /// since none are running
2812    pub new_plugin_args: Option<NewPluginArgs>,
2813    pub floating_pane_coordinates: Option<FloatingPaneCoordinates>,
2814}
2815
2816#[derive(Debug, Default, Clone)]
2817pub struct NewPluginArgs {
2818    pub should_float: Option<bool>,
2819    pub pane_id_to_replace: Option<PaneId>,
2820    pub pane_title: Option<String>,
2821    pub cwd: Option<PathBuf>,
2822    pub skip_cache: bool,
2823    pub should_focus: Option<bool>,
2824}
2825
2826#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
2827pub enum PaneId {
2828    Terminal(u32),
2829    Plugin(u32),
2830}
2831
2832impl Default for PaneId {
2833    fn default() -> Self {
2834        PaneId::Terminal(0)
2835    }
2836}
2837
2838impl FromStr for PaneId {
2839    type Err = Box<dyn std::error::Error>;
2840    fn from_str(stringified_pane_id: &str) -> Result<Self, Self::Err> {
2841        if let Some(terminal_stringified_pane_id) = stringified_pane_id.strip_prefix("terminal_") {
2842            u32::from_str_radix(terminal_stringified_pane_id, 10)
2843                .map(|id| PaneId::Terminal(id))
2844                .map_err(|e| e.into())
2845        } else if let Some(plugin_pane_id) = stringified_pane_id.strip_prefix("plugin_") {
2846            u32::from_str_radix(plugin_pane_id, 10)
2847                .map(|id| PaneId::Plugin(id))
2848                .map_err(|e| e.into())
2849        } else {
2850            u32::from_str_radix(&stringified_pane_id, 10)
2851                .map(|id| PaneId::Terminal(id))
2852                .map_err(|e| e.into())
2853        }
2854    }
2855}
2856
2857impl std::fmt::Display for PaneId {
2858    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2859        match self {
2860            PaneId::Terminal(id) => write!(f, "terminal_{}", id),
2861            PaneId::Plugin(id) => write!(f, "plugin_{}", id),
2862        }
2863    }
2864}
2865
2866impl MessageToPlugin {
2867    pub fn new(message_name: impl Into<String>) -> Self {
2868        MessageToPlugin {
2869            message_name: message_name.into(),
2870            ..Default::default()
2871        }
2872    }
2873    pub fn with_plugin_url(mut self, url: impl Into<String>) -> Self {
2874        self.plugin_url = Some(url.into());
2875        self
2876    }
2877    pub fn with_destination_plugin_id(mut self, destination_plugin_id: u32) -> Self {
2878        self.destination_plugin_id = Some(destination_plugin_id);
2879        self
2880    }
2881    pub fn with_plugin_config(mut self, plugin_config: BTreeMap<String, String>) -> Self {
2882        self.plugin_config = plugin_config;
2883        self
2884    }
2885    pub fn with_payload(mut self, payload: impl Into<String>) -> Self {
2886        self.message_payload = Some(payload.into());
2887        self
2888    }
2889    pub fn with_args(mut self, args: BTreeMap<String, String>) -> Self {
2890        self.message_args = args;
2891        self
2892    }
2893    pub fn with_floating_pane_coordinates(
2894        mut self,
2895        floating_pane_coordinates: FloatingPaneCoordinates,
2896    ) -> Self {
2897        self.floating_pane_coordinates = Some(floating_pane_coordinates);
2898        self
2899    }
2900    pub fn new_plugin_instance_should_float(mut self, should_float: bool) -> Self {
2901        let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
2902        new_plugin_args.should_float = Some(should_float);
2903        self
2904    }
2905    pub fn new_plugin_instance_should_replace_pane(mut self, pane_id: PaneId) -> Self {
2906        let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
2907        new_plugin_args.pane_id_to_replace = Some(pane_id);
2908        self
2909    }
2910    pub fn new_plugin_instance_should_have_pane_title(
2911        mut self,
2912        pane_title: impl Into<String>,
2913    ) -> Self {
2914        let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
2915        new_plugin_args.pane_title = Some(pane_title.into());
2916        self
2917    }
2918    pub fn new_plugin_instance_should_have_cwd(mut self, cwd: PathBuf) -> Self {
2919        let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
2920        new_plugin_args.cwd = Some(cwd);
2921        self
2922    }
2923    pub fn new_plugin_instance_should_skip_cache(mut self) -> Self {
2924        let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
2925        new_plugin_args.skip_cache = true;
2926        self
2927    }
2928    pub fn new_plugin_instance_should_be_focused(mut self) -> Self {
2929        let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
2930        new_plugin_args.should_focus = Some(true);
2931        self
2932    }
2933    pub fn has_cwd(&self) -> bool {
2934        self.new_plugin_args
2935            .as_ref()
2936            .map(|n| n.cwd.is_some())
2937            .unwrap_or(false)
2938    }
2939}
2940
2941#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
2942pub struct ConnectToSession {
2943    pub name: Option<String>,
2944    pub tab_position: Option<usize>,
2945    pub pane_id: Option<(u32, bool)>, // (id, is_plugin)
2946    pub layout: Option<LayoutInfo>,
2947    pub cwd: Option<PathBuf>,
2948}
2949
2950impl ConnectToSession {
2951    pub fn apply_layout_dir(&mut self, layout_dir: &PathBuf) {
2952        if let Some(LayoutInfo::File(file_path, _layout_metadata)) = self.layout.as_mut() {
2953            *file_path = Path::join(layout_dir, &file_path)
2954                .to_string_lossy()
2955                .to_string();
2956        }
2957    }
2958}
2959
2960#[derive(Debug, Default, Clone)]
2961pub struct PluginMessage {
2962    pub name: String,
2963    pub payload: String,
2964    pub worker_name: Option<String>,
2965}
2966
2967impl PluginMessage {
2968    pub fn new_to_worker(worker_name: &str, message: &str, payload: &str) -> Self {
2969        PluginMessage {
2970            name: message.to_owned(),
2971            payload: payload.to_owned(),
2972            worker_name: Some(worker_name.to_owned()),
2973        }
2974    }
2975    pub fn new_to_plugin(message: &str, payload: &str) -> Self {
2976        PluginMessage {
2977            name: message.to_owned(),
2978            payload: payload.to_owned(),
2979            worker_name: None,
2980        }
2981    }
2982}
2983
2984#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2985pub enum HttpVerb {
2986    Get,
2987    Post,
2988    Put,
2989    Delete,
2990}
2991
2992#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2993pub enum PipeSource {
2994    Cli(String), // String is the pipe_id of the CLI pipe (used for blocking/unblocking)
2995    Plugin(u32), // u32 is the lugin id
2996    Keybind,     // TODO: consider including the actual keybind here?
2997}
2998
2999#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3000pub struct PipeMessage {
3001    pub source: PipeSource,
3002    pub name: String,
3003    pub payload: Option<String>,
3004    pub args: BTreeMap<String, String>,
3005    pub is_private: bool,
3006}
3007
3008impl PipeMessage {
3009    pub fn new(
3010        source: PipeSource,
3011        name: impl Into<String>,
3012        payload: &Option<String>,
3013        args: &Option<BTreeMap<String, String>>,
3014        is_private: bool,
3015    ) -> Self {
3016        PipeMessage {
3017            source,
3018            name: name.into(),
3019            payload: payload.clone(),
3020            args: args.clone().unwrap_or_else(|| Default::default()),
3021            is_private,
3022        }
3023    }
3024}
3025
3026#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
3027pub struct FloatingPaneCoordinates {
3028    pub x: Option<PercentOrFixed>,
3029    pub y: Option<PercentOrFixed>,
3030    pub width: Option<PercentOrFixed>,
3031    pub height: Option<PercentOrFixed>,
3032    pub pinned: Option<bool>,
3033    pub borderless: Option<bool>,
3034}
3035
3036impl FloatingPaneCoordinates {
3037    pub fn new(
3038        x: Option<String>,
3039        y: Option<String>,
3040        width: Option<String>,
3041        height: Option<String>,
3042        pinned: Option<bool>,
3043        borderless: Option<bool>,
3044    ) -> Option<Self> {
3045        // Parse x/y coordinates - allows 0% or 0
3046        let x = x.and_then(|x| PercentOrFixed::from_str(&x).ok());
3047        let y = y.and_then(|y| PercentOrFixed::from_str(&y).ok());
3048
3049        // Parse width/height - reject 0% or 0
3050        let width = width.and_then(|w| {
3051            PercentOrFixed::from_str(&w)
3052                .ok()
3053                .and_then(|size| match size {
3054                    PercentOrFixed::Percent(0) => None,
3055                    PercentOrFixed::Fixed(0) => None,
3056                    _ => Some(size),
3057                })
3058        });
3059        let height = height.and_then(|h| {
3060            PercentOrFixed::from_str(&h)
3061                .ok()
3062                .and_then(|size| match size {
3063                    PercentOrFixed::Percent(0) => None,
3064                    PercentOrFixed::Fixed(0) => None,
3065                    _ => Some(size),
3066                })
3067        });
3068
3069        if x.is_none()
3070            && y.is_none()
3071            && width.is_none()
3072            && height.is_none()
3073            && pinned.is_none()
3074            && borderless.is_none()
3075        {
3076            None
3077        } else {
3078            Some(FloatingPaneCoordinates {
3079                x,
3080                y,
3081                width,
3082                height,
3083                pinned,
3084                borderless,
3085            })
3086        }
3087    }
3088    pub fn with_x_fixed(mut self, x: usize) -> Self {
3089        self.x = Some(PercentOrFixed::Fixed(x));
3090        self
3091    }
3092    pub fn with_x_percent(mut self, x: usize) -> Self {
3093        if x > 100 {
3094            eprintln!("x must be between 0 and 100");
3095            return self;
3096        }
3097        self.x = Some(PercentOrFixed::Percent(x));
3098        self
3099    }
3100    pub fn with_y_fixed(mut self, y: usize) -> Self {
3101        self.y = Some(PercentOrFixed::Fixed(y));
3102        self
3103    }
3104    pub fn with_y_percent(mut self, y: usize) -> Self {
3105        if y > 100 {
3106            eprintln!("y must be between 0 and 100");
3107            return self;
3108        }
3109        self.y = Some(PercentOrFixed::Percent(y));
3110        self
3111    }
3112    pub fn with_width_fixed(mut self, width: usize) -> Self {
3113        self.width = Some(PercentOrFixed::Fixed(width));
3114        self
3115    }
3116    pub fn with_width_percent(mut self, width: usize) -> Self {
3117        if width > 100 {
3118            eprintln!("width must be between 0 and 100");
3119            return self;
3120        }
3121        self.width = Some(PercentOrFixed::Percent(width));
3122        self
3123    }
3124    pub fn with_height_fixed(mut self, height: usize) -> Self {
3125        self.height = Some(PercentOrFixed::Fixed(height));
3126        self
3127    }
3128    pub fn with_height_percent(mut self, height: usize) -> Self {
3129        if height > 100 {
3130            eprintln!("height must be between 0 and 100");
3131            return self;
3132        }
3133        self.height = Some(PercentOrFixed::Percent(height));
3134        self
3135    }
3136}
3137
3138impl From<PaneGeom> for FloatingPaneCoordinates {
3139    fn from(pane_geom: PaneGeom) -> Self {
3140        FloatingPaneCoordinates {
3141            x: Some(PercentOrFixed::Fixed(pane_geom.x)),
3142            y: Some(PercentOrFixed::Fixed(pane_geom.y)),
3143            width: Some(PercentOrFixed::Fixed(pane_geom.cols.as_usize())),
3144            height: Some(PercentOrFixed::Fixed(pane_geom.rows.as_usize())),
3145            pinned: Some(pane_geom.is_pinned),
3146            borderless: None,
3147        }
3148    }
3149}
3150
3151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3152pub struct OriginatingPlugin {
3153    pub plugin_id: u32,
3154    pub client_id: ClientId,
3155    pub context: Context,
3156}
3157
3158impl OriginatingPlugin {
3159    pub fn new(plugin_id: u32, client_id: ClientId, context: Context) -> Self {
3160        OriginatingPlugin {
3161            plugin_id,
3162            client_id,
3163            context,
3164        }
3165    }
3166}
3167
3168#[derive(ArgEnum, Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
3169pub enum WebSharing {
3170    #[serde(alias = "on")]
3171    On,
3172    #[serde(alias = "off")]
3173    Off,
3174    #[serde(alias = "disabled")]
3175    Disabled,
3176}
3177
3178impl Default for WebSharing {
3179    fn default() -> Self {
3180        Self::Off
3181    }
3182}
3183
3184impl WebSharing {
3185    pub fn is_on(&self) -> bool {
3186        match self {
3187            WebSharing::On => true,
3188            _ => false,
3189        }
3190    }
3191    pub fn web_clients_allowed(&self) -> bool {
3192        match self {
3193            WebSharing::On => true,
3194            _ => false,
3195        }
3196    }
3197    pub fn sharing_is_disabled(&self) -> bool {
3198        match self {
3199            WebSharing::Disabled => true,
3200            _ => false,
3201        }
3202    }
3203    pub fn set_sharing(&mut self) -> bool {
3204        // returns true if successfully set sharing
3205        match self {
3206            WebSharing::On => true,
3207            WebSharing::Off => {
3208                *self = WebSharing::On;
3209                true
3210            },
3211            WebSharing::Disabled => false,
3212        }
3213    }
3214    pub fn set_not_sharing(&mut self) -> bool {
3215        // returns true if successfully set not sharing
3216        match self {
3217            WebSharing::On => {
3218                *self = WebSharing::Off;
3219                true
3220            },
3221            WebSharing::Off => true,
3222            WebSharing::Disabled => false,
3223        }
3224    }
3225}
3226
3227impl FromStr for WebSharing {
3228    type Err = String;
3229    fn from_str(s: &str) -> Result<Self, Self::Err> {
3230        match s {
3231            "On" | "on" => Ok(Self::On),
3232            "Off" | "off" => Ok(Self::Off),
3233            "Disabled" | "disabled" => Ok(Self::Disabled),
3234            _ => Err(format!("No such option: {}", s)),
3235        }
3236    }
3237}
3238
3239#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
3240pub enum NewPanePlacement {
3241    NoPreference {
3242        borderless: Option<bool>,
3243    },
3244    Tiled {
3245        direction: Option<Direction>,
3246        borderless: Option<bool>,
3247    },
3248    Floating(Option<FloatingPaneCoordinates>),
3249    InPlace {
3250        pane_id_to_replace: Option<PaneId>,
3251        close_replaced_pane: bool,
3252        borderless: Option<bool>,
3253    },
3254    Stacked {
3255        pane_id_to_stack_under: Option<PaneId>,
3256        borderless: Option<bool>,
3257    },
3258}
3259
3260impl Default for NewPanePlacement {
3261    fn default() -> Self {
3262        NewPanePlacement::NoPreference { borderless: None }
3263    }
3264}
3265
3266impl NewPanePlacement {
3267    pub fn with_floating_pane_coordinates(
3268        floating_pane_coordinates: Option<FloatingPaneCoordinates>,
3269    ) -> Self {
3270        NewPanePlacement::Floating(floating_pane_coordinates)
3271    }
3272    pub fn with_should_be_in_place(
3273        self,
3274        should_be_in_place: bool,
3275        close_replaced_pane: bool,
3276    ) -> Self {
3277        if should_be_in_place {
3278            NewPanePlacement::InPlace {
3279                pane_id_to_replace: None,
3280                close_replaced_pane,
3281                borderless: None,
3282            }
3283        } else {
3284            self
3285        }
3286    }
3287    pub fn with_pane_id_to_replace(
3288        pane_id_to_replace: Option<PaneId>,
3289        close_replaced_pane: bool,
3290    ) -> Self {
3291        NewPanePlacement::InPlace {
3292            pane_id_to_replace,
3293            close_replaced_pane,
3294            borderless: None,
3295        }
3296    }
3297    pub fn should_float(&self) -> Option<bool> {
3298        match self {
3299            NewPanePlacement::Floating(_) => Some(true),
3300            NewPanePlacement::Tiled { .. } => Some(false),
3301            _ => None,
3302        }
3303    }
3304    pub fn floating_pane_coordinates(&self) -> Option<FloatingPaneCoordinates> {
3305        match self {
3306            NewPanePlacement::Floating(floating_pane_coordinates) => {
3307                floating_pane_coordinates.clone()
3308            },
3309            _ => None,
3310        }
3311    }
3312    pub fn should_stack(&self) -> bool {
3313        match self {
3314            NewPanePlacement::Stacked { .. } => true,
3315            _ => false,
3316        }
3317    }
3318    pub fn id_of_stack_root(&self) -> Option<PaneId> {
3319        match self {
3320            NewPanePlacement::Stacked {
3321                pane_id_to_stack_under,
3322                ..
3323            } => *pane_id_to_stack_under,
3324            _ => None,
3325        }
3326    }
3327    pub fn get_borderless(&self) -> Option<bool> {
3328        match self {
3329            NewPanePlacement::NoPreference { borderless } => *borderless,
3330            NewPanePlacement::Tiled { borderless, .. } => *borderless,
3331            NewPanePlacement::Floating(coords) => coords.as_ref().and_then(|c| c.borderless),
3332            NewPanePlacement::InPlace { borderless, .. } => *borderless,
3333            NewPanePlacement::Stacked { borderless, .. } => *borderless,
3334        }
3335    }
3336}
3337
3338type Context = BTreeMap<String, String>;
3339
3340#[derive(Debug, Clone, EnumDiscriminants, Display)]
3341#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))]
3342#[strum_discriminants(name(CommandType))]
3343pub enum PluginCommand {
3344    Subscribe(HashSet<EventType>),
3345    Unsubscribe(HashSet<EventType>),
3346    SetSelectable(bool),
3347    ShowCursor(Option<(usize, usize)>),
3348    GetPluginIds,
3349    GetZellijVersion,
3350    OpenFile(FileToOpen, Context),
3351    OpenFileFloating(FileToOpen, Option<FloatingPaneCoordinates>, Context),
3352    OpenTerminal(FileToOpen), // only used for the path as cwd
3353    OpenTerminalFloating(FileToOpen, Option<FloatingPaneCoordinates>), // only used for the path as cwd
3354    OpenCommandPane(CommandToRun, Context),
3355    OpenCommandPaneFloating(CommandToRun, Option<FloatingPaneCoordinates>, Context),
3356    SwitchTabTo(u32), // tab index
3357    SetTimeout(f64),  // seconds
3358    ExecCmd(Vec<String>),
3359    PostMessageTo(PluginMessage),
3360    PostMessageToPlugin(PluginMessage),
3361    HideSelf,
3362    ShowSelf(bool), // bool - should float if hidden
3363    SwitchToMode(InputMode),
3364    NewTabsWithLayout(String), // raw kdl layout
3365    NewTab {
3366        name: Option<String>,
3367        cwd: Option<String>,
3368    },
3369    GoToNextTab,
3370    GoToPreviousTab,
3371    Resize(Resize),
3372    ResizeWithDirection(ResizeStrategy),
3373    FocusNextPane,
3374    FocusPreviousPane,
3375    MoveFocus(Direction),
3376    MoveFocusOrTab(Direction),
3377    Detach,
3378    EditScrollback,
3379    Write(Vec<u8>), // bytes
3380    WriteChars(String),
3381    ToggleTab,
3382    MovePane,
3383    MovePaneWithDirection(Direction),
3384    ClearScreen,
3385    ScrollUp,
3386    ScrollDown,
3387    ScrollToTop,
3388    ScrollToBottom,
3389    PageScrollUp,
3390    PageScrollDown,
3391    ToggleFocusFullscreen,
3392    TogglePaneFrames,
3393    TogglePaneEmbedOrEject,
3394    UndoRenamePane,
3395    CloseFocus,
3396    ToggleActiveTabSync,
3397    CloseFocusedTab,
3398    UndoRenameTab,
3399    QuitZellij,
3400    PreviousSwapLayout,
3401    NextSwapLayout,
3402    GoToTabName(String),
3403    FocusOrCreateTab(String),
3404    GoToTab(u32),                       // tab index
3405    StartOrReloadPlugin(String),        // plugin url (eg. file:/path/to/plugin.wasm)
3406    CloseTerminalPane(u32),             // terminal pane id
3407    ClosePluginPane(u32),               // plugin pane id
3408    FocusTerminalPane(u32, bool, bool), // terminal pane id, should_float_if_hidden, should_be_in_place_if_hidden
3409    FocusPluginPane(u32, bool, bool), // plugin pane id, should_float_if_hidden, should_be_in_place_if_hidden
3410    RenameTerminalPane(u32, String),  // terminal pane id, new name
3411    RenamePluginPane(u32, String),    // plugin pane id, new name
3412    RenameTab(u32, String),           // tab index, new name
3413    ReportPanic(String),              // stringified panic
3414    RequestPluginPermissions(Vec<PermissionType>),
3415    SwitchSession(ConnectToSession),
3416    DeleteDeadSession(String),       // String -> session name
3417    DeleteAllDeadSessions,           // String -> session name
3418    OpenTerminalInPlace(FileToOpen), // only used for the path as cwd
3419    OpenFileInPlace(FileToOpen, Context),
3420    OpenCommandPaneInPlace(CommandToRun, Context),
3421    RunCommand(
3422        Vec<String>,              // command
3423        BTreeMap<String, String>, // env_variables
3424        PathBuf,                  // cwd
3425        BTreeMap<String, String>, // context
3426    ),
3427    WebRequest(
3428        String, // url
3429        HttpVerb,
3430        BTreeMap<String, String>, // headers
3431        Vec<u8>,                  // body
3432        BTreeMap<String, String>, // context
3433    ),
3434    RenameSession(String),         // String -> new session name
3435    UnblockCliPipeInput(String),   // String => pipe name
3436    BlockCliPipeInput(String),     // String => pipe name
3437    CliPipeOutput(String, String), // String => pipe name, String => output
3438    MessageToPlugin(MessageToPlugin),
3439    DisconnectOtherClients,
3440    KillSessions(Vec<String>), // one or more session names
3441    ScanHostFolder(PathBuf),   // TODO: rename to ScanHostFolder
3442    WatchFilesystem,
3443    DumpSessionLayout {
3444        tab_index: Option<usize>,
3445    },
3446    CloseSelf,
3447    NewTabsWithLayoutInfo(LayoutInfo),
3448    Reconfigure(String, bool), // String -> stringified configuration, bool -> save configuration
3449    // file to disk
3450    HidePaneWithId(PaneId),
3451    ShowPaneWithId(PaneId, bool, bool), // bools -> should_float_if_hidden, should_focus_pane
3452    OpenCommandPaneBackground(CommandToRun, Context),
3453    RerunCommandPane(u32), // u32  - terminal pane id
3454    ResizePaneIdWithDirection(ResizeStrategy, PaneId),
3455    EditScrollbackForPaneWithId(PaneId),
3456    GetPaneScrollback {
3457        pane_id: PaneId,
3458        get_full_scrollback: bool,
3459    },
3460    WriteToPaneId(Vec<u8>, PaneId),
3461    WriteCharsToPaneId(String, PaneId),
3462    SendSigintToPaneId(PaneId),
3463    SendSigkillToPaneId(PaneId),
3464    GetPanePid {
3465        pane_id: PaneId,
3466    },
3467    GetPaneRunningCommand {
3468        pane_id: PaneId,
3469    },
3470    GetPaneCwd {
3471        pane_id: PaneId,
3472    },
3473    MovePaneWithPaneId(PaneId),
3474    MovePaneWithPaneIdInDirection(PaneId, Direction),
3475    ClearScreenForPaneId(PaneId),
3476    ScrollUpInPaneId(PaneId),
3477    ScrollDownInPaneId(PaneId),
3478    ScrollToTopInPaneId(PaneId),
3479    ScrollToBottomInPaneId(PaneId),
3480    PageScrollUpInPaneId(PaneId),
3481    PageScrollDownInPaneId(PaneId),
3482    TogglePaneIdFullscreen(PaneId),
3483    TogglePaneEmbedOrEjectForPaneId(PaneId),
3484    CloseTabWithIndex(usize), // usize - tab_index
3485    BreakPanesToNewTab(Vec<PaneId>, Option<String>, bool), // bool -
3486    // should_change_focus_to_new_tab,
3487    // Option<String> - optional name for
3488    // the new tab
3489    BreakPanesToTabWithIndex(Vec<PaneId>, usize, bool), // usize - tab_index, bool -
3490    // should_change_focus_to_new_tab
3491    SwitchTabToId(u64),                            // u64 - tab_id
3492    GoToTabWithId(u64),                            // u64 - tab_id
3493    CloseTabWithId(u64),                           // u64 - tab_id
3494    RenameTabWithId(u64, String),                  // u64 - tab_id, String - new name
3495    BreakPanesToTabWithId(Vec<PaneId>, u64, bool), // u64 - tab_id, bool -
3496    // should_change_focus_to_target_tab
3497    ReloadPlugin(u32), // u32 - plugin pane id
3498    LoadNewPlugin {
3499        url: String,
3500        config: BTreeMap<String, String>,
3501        load_in_background: bool,
3502        skip_plugin_cache: bool,
3503    },
3504    RebindKeys {
3505        keys_to_rebind: Vec<(InputMode, KeyWithModifier, Vec<Action>)>,
3506        keys_to_unbind: Vec<(InputMode, KeyWithModifier)>,
3507        write_config_to_disk: bool,
3508    },
3509    ListClients,
3510    ChangeHostFolder(PathBuf),
3511    SetFloatingPanePinned(PaneId, bool), // bool -> should be pinned
3512    StackPanes(Vec<PaneId>),
3513    ChangeFloatingPanesCoordinates(Vec<(PaneId, FloatingPaneCoordinates)>),
3514    TogglePaneBorderless(PaneId),
3515    SetPaneBorderless(PaneId, bool),
3516    OpenCommandPaneNearPlugin(CommandToRun, Context),
3517    OpenTerminalNearPlugin(FileToOpen),
3518    OpenTerminalFloatingNearPlugin(FileToOpen, Option<FloatingPaneCoordinates>),
3519    OpenTerminalInPlaceOfPlugin(FileToOpen, bool), // bool -> close_plugin_after_replace
3520    OpenCommandPaneFloatingNearPlugin(CommandToRun, Option<FloatingPaneCoordinates>, Context),
3521    OpenCommandPaneInPlaceOfPlugin(CommandToRun, bool, Context), // bool ->
3522    // close_plugin_after_replace
3523    OpenFileNearPlugin(FileToOpen, Context),
3524    OpenFileFloatingNearPlugin(FileToOpen, Option<FloatingPaneCoordinates>, Context),
3525    StartWebServer,
3526    StopWebServer,
3527    ShareCurrentSession,
3528    StopSharingCurrentSession,
3529    OpenFileInPlaceOfPlugin(FileToOpen, bool, Context), // bool -> close_plugin_after_replace
3530    GroupAndUngroupPanes(Vec<PaneId>, Vec<PaneId>, bool), // panes to group, panes to ungroup,
3531    // bool -> for all clients
3532    HighlightAndUnhighlightPanes(Vec<PaneId>, Vec<PaneId>), // panes to highlight, panes to
3533    // unhighlight
3534    CloseMultiplePanes(Vec<PaneId>),
3535    FloatMultiplePanes(Vec<PaneId>),
3536    EmbedMultiplePanes(Vec<PaneId>),
3537    QueryWebServerStatus,
3538    SetSelfMouseSelectionSupport(bool),
3539    GenerateWebLoginToken(Option<String>, bool), // (token_label, read_only)
3540    RevokeWebLoginToken(String), // String -> token id (provided name or generated id)
3541    ListWebLoginTokens,
3542    RevokeAllWebLoginTokens,
3543    RenameWebLoginToken(String, String), // (original_name, new_name)
3544    InterceptKeyPresses,
3545    ClearKeyPressesIntercepts,
3546    ReplacePaneWithExistingPane(PaneId, PaneId, bool), // (pane id to replace, pane id of existing,
3547    // suppress_replaced_pane)
3548    RunAction(Action, BTreeMap<String, String>),
3549    CopyToClipboard(String), // text to copy
3550    OverrideLayout(
3551        LayoutInfo,
3552        bool,                     // retain_existing_terminal_panes
3553        bool,                     // retain_existing_plugin_panes
3554        bool,                     // apply_only_to_active_tab,
3555        BTreeMap<String, String>, // context
3556    ),
3557    SaveLayout {
3558        layout_name: String,
3559        layout_kdl: String,
3560        overwrite: bool,
3561    },
3562    DeleteLayout {
3563        layout_name: String,
3564    },
3565    RenameLayout {
3566        old_layout_name: String,
3567        new_layout_name: String,
3568    },
3569    EditLayout {
3570        layout_name: String,
3571        context: Context,
3572    },
3573    GenerateRandomName,
3574    DumpLayout(String),
3575    ParseLayout(String), // String contains raw KDL layout
3576    GetLayoutDir,
3577    GetFocusedPaneInfo,
3578    SaveSession,
3579    CurrentSessionLastSavedTime,
3580    GetPaneInfo(PaneId),
3581    GetTabInfo(usize), // tab_id
3582    GetSessionEnvironmentVariables,
3583    OpenCommandPaneInNewTab(CommandToRun, Context),
3584    OpenPluginPaneInNewTab {
3585        plugin_url: String,
3586        configuration: BTreeMap<String, String>,
3587        context: Context,
3588    },
3589    OpenEditorPaneInNewTab(FileToOpen, Context),
3590    OpenCommandPaneInPlaceOfPaneId(PaneId, CommandToRun, bool, Context), // bool = close_replaced_pane
3591    OpenTerminalPaneInPlaceOfPaneId(PaneId, FileToOpen, bool),
3592    OpenEditPaneInPlaceOfPaneId(PaneId, FileToOpen, bool, Context),
3593    HideFloatingPanes {
3594        tab_id: Option<usize>,
3595    },
3596    ShowFloatingPanes {
3597        tab_id: Option<usize>,
3598    },
3599    SetPaneColor(PaneId, Option<String>, Option<String>), // (pane_id, fg, bg)
3600    SetPaneRegexHighlights(PaneId, Vec<RegexHighlight>),
3601    ClearPaneHighlights(PaneId),
3602    OpenPluginPaneFloating {
3603        plugin_url: String,
3604        configuration: BTreeMap<String, String>,
3605        floating_pane_coordinates: Option<FloatingPaneCoordinates>,
3606        context: BTreeMap<String, String>,
3607    },
3608    ListWindowsVolumes,
3609    GetSessionList,
3610    KillSessionsAndReply(Vec<String>), // one or more session names; sends a response back
3611    DeleteDeadSessionAndReply(String), // session name; sends a response back
3612    DeleteAllDeadSessionsAndReply,     // no payload; sends a response back
3613}
3614
3615// Response type for plugin API methods that open a pane in a new tab
3616#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3617pub struct OpenPaneInNewTabResponse {
3618    pub tab_id: Option<usize>,
3619    pub pane_id: Option<PaneId>,
3620}
3621
3622// Response types for plugin API methods that create tabs
3623pub type NewTabResponse = Option<usize>;
3624pub type NewTabsResponse = Vec<usize>;
3625pub type FocusOrCreateTabResponse = Option<usize>;
3626pub type BreakPanesToNewTabResponse = Option<usize>;
3627pub type BreakPanesToTabWithIndexResponse = Option<usize>;
3628pub type BreakPanesToTabWithIdResponse = Option<usize>;
3629
3630// Response types for plugin API methods that create panes
3631pub type OpenFileResponse = Option<PaneId>;
3632pub type OpenFileFloatingResponse = Option<PaneId>;
3633pub type OpenFileInPlaceResponse = Option<PaneId>;
3634pub type OpenFileNearPluginResponse = Option<PaneId>;
3635pub type OpenFileFloatingNearPluginResponse = Option<PaneId>;
3636pub type OpenFileInPlaceOfPluginResponse = Option<PaneId>;
3637
3638pub type OpenTerminalResponse = Option<PaneId>;
3639pub type OpenTerminalFloatingResponse = Option<PaneId>;
3640pub type OpenTerminalInPlaceResponse = Option<PaneId>;
3641pub type OpenTerminalNearPluginResponse = Option<PaneId>;
3642pub type OpenTerminalFloatingNearPluginResponse = Option<PaneId>;
3643pub type OpenTerminalInPlaceOfPluginResponse = Option<PaneId>;
3644
3645pub type OpenCommandPaneResponse = Option<PaneId>;
3646pub type OpenCommandPaneFloatingResponse = Option<PaneId>;
3647pub type OpenCommandPaneInPlaceResponse = Option<PaneId>;
3648pub type OpenCommandPaneNearPluginResponse = Option<PaneId>;
3649pub type OpenCommandPaneFloatingNearPluginResponse = Option<PaneId>;
3650pub type OpenCommandPaneInPlaceOfPluginResponse = Option<PaneId>;
3651pub type OpenCommandPaneBackgroundResponse = Option<PaneId>;
3652pub type OpenCommandPaneInPlaceOfPaneIdResponse = Option<PaneId>;
3653pub type OpenTerminalPaneInPlaceOfPaneIdResponse = Option<PaneId>;
3654pub type OpenEditPaneInPlaceOfPaneIdResponse = Option<PaneId>;
3655pub type OpenPluginPaneFloatingResponse = Option<PaneId>;
3656
3657#[test]
3658pub fn can_parse_unicode_bare_keys() {
3659    let key = "1087"; // п
3660    assert_eq!(
3661        BareKey::from_bytes_with_u(&key.as_bytes()),
3662        Some(BareKey::Char('п')),
3663        "Can parse a bare 'п' keypress"
3664    );
3665    let key = "1255"; // ӧ
3666    assert_eq!(
3667        BareKey::from_bytes_with_u(&key.as_bytes()),
3668        Some(BareKey::Char('ӧ')),
3669        "Can parse a bare 'ӧ' keypress"
3670    );
3671    let key = "1098"; // ъ
3672    assert_eq!(
3673        BareKey::from_bytes_with_u(&key.as_bytes()),
3674        Some(BareKey::Char('ъ')),
3675        "Can parse a bare 'ъ' keypress"
3676    );
3677}