Skip to main content

zellij_utils/
data.rs

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