use std::path::PathBuf;
use std::time::Instant;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MouseProtocol {
#[default]
Off,
X10,
Btn,
Any,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MouseEncoding {
#[default]
X10,
Sgr,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MouseMode {
pub protocol: MouseProtocol,
pub encoding: MouseEncoding,
}
impl MouseMode {
pub fn is_off(&self) -> bool {
matches!(self.protocol, MouseProtocol::Off)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct KittyKbdFlags(pub u8);
impl KittyKbdFlags {
pub const DISAMBIGUATE: u8 = 0b00001;
pub const REPORT_EVENTS: u8 = 0b00010;
pub const REPORT_ALTERNATES: u8 = 0b00100;
pub const REPORT_ALL_AS_ESCAPES: u8 = 0b01000;
pub const REPORT_ASSOCIATED_TEXT: u8 = 0b10000;
pub const ALL: u8 = 0b11111;
pub fn bits(self) -> u8 {
self.0 & Self::ALL
}
}
#[derive(Clone, Debug, Default)]
pub struct KittyKbdStack {
entries: Vec<KittyKbdFlags>,
}
impl KittyKbdStack {
pub fn new() -> Self {
Self::default()
}
pub fn active(&self) -> KittyKbdFlags {
self.entries.last().copied().unwrap_or_default()
}
pub fn push(&mut self, flags: KittyKbdFlags) {
const MAX_DEPTH: usize = 32;
if self.entries.len() >= MAX_DEPTH {
self.entries.remove(0);
}
self.entries.push(flags);
}
pub fn pop(&mut self, n: usize) {
let n = n.max(1);
for _ in 0..n {
if self.entries.pop().is_none() {
break;
}
}
}
pub fn modify_top(&mut self, flags: KittyKbdFlags, mode: u8) {
if self.entries.is_empty() {
self.entries.push(KittyKbdFlags(0));
}
let top = self.entries.last_mut().unwrap();
match mode {
1 => *top = flags,
2 => *top = KittyKbdFlags(top.0 | flags.0),
3 => *top = KittyKbdFlags(top.0 & !flags.0),
_ => {}
}
}
pub fn depth(&self) -> usize {
self.entries.len()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Osc52SetPolicy {
Allow,
#[default]
Confirm,
Deny,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Osc52GetPolicy {
Allow,
#[default]
Deny,
}
#[derive(Clone, Copy, Debug)]
pub struct ClipboardPolicy {
pub set: Osc52SetPolicy,
pub get: Osc52GetPolicy,
pub max_bytes: usize,
}
impl Default for ClipboardPolicy {
fn default() -> Self {
Self {
set: Osc52SetPolicy::Confirm,
get: Osc52GetPolicy::Deny,
max_bytes: 1024 * 1024,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Osc52Decision {
#[default]
Pending,
Allowed,
Denied,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Rgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Rgb {
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
pub fn to_xterm_rgb_str(self) -> String {
format!(
"rgb:{:02x}{:02x}/{:02x}{:02x}/{:02x}{:02x}",
self.r, self.r, self.g, self.g, self.b, self.b
)
}
}
#[derive(Clone, Debug)]
pub struct ThemePalette {
pub fg: Option<Rgb>,
pub bg: Option<Rgb>,
pub cursor: Option<Rgb>,
pub palette: [Option<Rgb>; 256],
}
impl Default for ThemePalette {
fn default() -> Self {
Self {
fg: None,
bg: None,
cursor: None,
palette: [None; 256],
}
}
}
impl ThemePalette {
pub fn is_active(&self) -> bool {
self.fg.is_some()
|| self.bg.is_some()
|| self.cursor.is_some()
|| self.palette.iter().any(|c| c.is_some())
}
}
#[derive(Clone, Debug, Default)]
pub struct PaneTerminalState {
pub bracketed_paste: bool,
pub focus_reporting: bool,
pub mouse_mode: MouseMode,
pub kitty_kbd: KittyKbdStack,
pub reported_cwd: Option<(PathBuf, Instant)>,
pub osc52_decision: Osc52Decision,
pub osc52_pending_confirm: Vec<Vec<u8>>,
}
impl PaneTerminalState {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
*self = Self::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kitty_stack_push_pop_top() {
let mut s = KittyKbdStack::new();
assert_eq!(s.active().bits(), 0);
s.push(KittyKbdFlags(0b00001));
assert_eq!(s.active().bits(), 0b00001);
s.push(KittyKbdFlags(0b01111));
assert_eq!(s.active().bits(), 0b01111);
s.pop(1);
assert_eq!(s.active().bits(), 0b00001);
s.pop(5); assert_eq!(s.depth(), 0);
assert_eq!(s.active().bits(), 0);
}
#[test]
fn kitty_stack_modify_top() {
let mut s = KittyKbdStack::new();
s.modify_top(KittyKbdFlags(0b00101), 1);
assert_eq!(s.active().bits(), 0b00101);
s.modify_top(KittyKbdFlags(0b01000), 2);
assert_eq!(s.active().bits(), 0b01101);
s.modify_top(KittyKbdFlags(0b00100), 3);
assert_eq!(s.active().bits(), 0b01001);
let before = s.active().bits();
s.modify_top(KittyKbdFlags(0xff), 99);
assert_eq!(s.active().bits(), before);
}
#[test]
fn kitty_stack_capped_depth() {
let mut s = KittyKbdStack::new();
for i in 0..40 {
s.push(KittyKbdFlags(i as u8));
}
assert!(s.depth() <= 32);
assert_eq!(s.active().bits(), 39 & KittyKbdFlags::ALL);
}
#[test]
fn rgb_xterm_format_roundtrip() {
let c = Rgb::new(0x12, 0xab, 0xff);
assert_eq!(c.to_xterm_rgb_str(), "rgb:1212/abab/ffff");
}
#[test]
fn theme_palette_inactive_when_empty() {
let p = ThemePalette::default();
assert!(!p.is_active());
}
#[test]
fn theme_palette_active_when_any_field_set() {
let mut p = ThemePalette::default();
p.fg = Some(Rgb::new(255, 255, 255));
assert!(p.is_active());
}
#[test]
fn clipboard_policy_defaults_secure() {
let p = ClipboardPolicy::default();
assert_eq!(p.set, Osc52SetPolicy::Confirm);
assert_eq!(p.get, Osc52GetPolicy::Deny);
assert_eq!(p.max_bytes, 1024 * 1024);
}
#[test]
fn pane_state_reset_clears_everything() {
let mut s = PaneTerminalState::new();
s.bracketed_paste = true;
s.focus_reporting = true;
s.mouse_mode = MouseMode {
protocol: MouseProtocol::Btn,
encoding: MouseEncoding::Sgr,
};
s.kitty_kbd.push(KittyKbdFlags(0b11111));
s.osc52_decision = Osc52Decision::Allowed;
s.reset();
assert!(!s.bracketed_paste);
assert!(!s.focus_reporting);
assert!(s.mouse_mode.is_off());
assert_eq!(s.kitty_kbd.depth(), 0);
assert_eq!(s.osc52_decision, Osc52Decision::Pending);
}
}