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