use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TerminalMode {
#[default]
AltScreen,
Inline,
}
impl TerminalMode {
#[must_use]
pub const fn uses_alt_screen(self) -> bool {
matches!(self, Self::AltScreen)
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::AltScreen => "Alt Screen",
Self::Inline => "Inline",
}
}
}
impl std::fmt::Display for TerminalMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TerminalEvent {
Resized {
width: u16,
height: u16,
},
Reconnected,
FocusGained,
FocusLost,
ModeChange(TerminalMode),
ResizeBurst {
final_width: u16,
final_height: u16,
burst_count: u32,
},
}
#[derive(Debug, Clone)]
pub struct TerminalState {
pub mode: TerminalMode,
pub width: u16,
pub height: u16,
pub focused: bool,
pub connected: bool,
pending_resizes: u32,
needs_full_redraw: bool,
}
impl TerminalState {
#[must_use]
pub const fn new(mode: TerminalMode) -> Self {
Self {
mode,
width: 80,
height: 24,
focused: true,
connected: true,
pending_resizes: 0,
needs_full_redraw: true, }
}
pub fn handle_event(&mut self, event: &TerminalEvent) -> bool {
match event {
TerminalEvent::Resized { width, height } => {
let changed = self.width != *width || self.height != *height;
self.width = *width;
self.height = *height;
self.pending_resizes += 1;
if changed {
self.needs_full_redraw = true;
}
changed
}
TerminalEvent::Reconnected => {
self.connected = true;
self.needs_full_redraw = true;
true
}
TerminalEvent::FocusGained => {
let changed = !self.focused;
self.focused = true;
changed
}
TerminalEvent::FocusLost => {
let changed = self.focused;
self.focused = false;
changed
}
TerminalEvent::ModeChange(new_mode) => {
let changed = self.mode != *new_mode;
self.mode = *new_mode;
if changed {
self.needs_full_redraw = true;
}
changed
}
TerminalEvent::ResizeBurst {
final_width,
final_height,
burst_count,
} => {
let changed = self.width != *final_width || self.height != *final_height;
self.width = *final_width;
self.height = *final_height;
self.pending_resizes += burst_count;
if changed {
self.needs_full_redraw = true;
}
changed
}
}
}
#[must_use]
pub const fn needs_full_redraw(&self) -> bool {
self.needs_full_redraw
}
pub const fn acknowledge_redraw(&mut self) {
self.needs_full_redraw = false;
self.pending_resizes = 0;
}
pub const fn mark_disconnected(&mut self) {
self.connected = false;
}
#[must_use]
pub const fn pending_resizes(&self) -> u32 {
self.pending_resizes
}
#[must_use]
pub const fn is_connected(&self) -> bool {
self.connected
}
}
impl Default for TerminalState {
fn default() -> Self {
Self::new(TerminalMode::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn terminal_mode_default() {
assert_eq!(TerminalMode::default(), TerminalMode::AltScreen);
}
#[test]
fn terminal_mode_alt_screen() {
assert!(TerminalMode::AltScreen.uses_alt_screen());
assert!(!TerminalMode::Inline.uses_alt_screen());
}
#[test]
fn terminal_mode_labels() {
assert_eq!(TerminalMode::AltScreen.label(), "Alt Screen");
assert_eq!(TerminalMode::Inline.label(), "Inline");
}
#[test]
fn terminal_mode_display() {
assert_eq!(TerminalMode::AltScreen.to_string(), "Alt Screen");
assert_eq!(TerminalMode::Inline.to_string(), "Inline");
}
#[test]
fn terminal_mode_serde_roundtrip() {
for mode in [TerminalMode::AltScreen, TerminalMode::Inline] {
let json = serde_json::to_string(&mode).unwrap();
let decoded: TerminalMode = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, mode);
}
}
#[test]
fn terminal_state_defaults() {
let state = TerminalState::new(TerminalMode::AltScreen);
assert_eq!(state.mode, TerminalMode::AltScreen);
assert_eq!(state.width, 80);
assert_eq!(state.height, 24);
assert!(state.focused);
assert!(state.connected);
assert!(state.needs_full_redraw()); }
#[test]
fn terminal_state_resize() {
let mut state = TerminalState::new(TerminalMode::AltScreen);
state.acknowledge_redraw();
assert!(!state.needs_full_redraw());
let changed = state.handle_event(&TerminalEvent::Resized {
width: 120,
height: 40,
});
assert!(changed);
assert_eq!(state.width, 120);
assert_eq!(state.height, 40);
assert!(state.needs_full_redraw());
assert_eq!(state.pending_resizes(), 1);
}
#[test]
fn terminal_state_same_size_no_change() {
let mut state = TerminalState::new(TerminalMode::AltScreen);
state.acknowledge_redraw();
let changed = state.handle_event(&TerminalEvent::Resized {
width: 80,
height: 24,
});
assert!(!changed);
}
#[test]
fn terminal_state_reconnect() {
let mut state = TerminalState::new(TerminalMode::AltScreen);
state.acknowledge_redraw();
state.mark_disconnected();
assert!(!state.is_connected());
let changed = state.handle_event(&TerminalEvent::Reconnected);
assert!(changed);
assert!(state.is_connected());
assert!(state.needs_full_redraw());
}
#[test]
fn terminal_state_focus_gain_loss() {
let mut state = TerminalState::new(TerminalMode::AltScreen);
let changed = state.handle_event(&TerminalEvent::FocusLost);
assert!(changed);
assert!(!state.focused);
let changed = state.handle_event(&TerminalEvent::FocusGained);
assert!(changed);
assert!(state.focused);
let changed = state.handle_event(&TerminalEvent::FocusGained);
assert!(!changed);
}
#[test]
fn terminal_state_mode_change() {
let mut state = TerminalState::new(TerminalMode::AltScreen);
state.acknowledge_redraw();
let changed = state.handle_event(&TerminalEvent::ModeChange(TerminalMode::Inline));
assert!(changed);
assert_eq!(state.mode, TerminalMode::Inline);
assert!(state.needs_full_redraw());
state.acknowledge_redraw();
let changed = state.handle_event(&TerminalEvent::ModeChange(TerminalMode::Inline));
assert!(!changed);
}
#[test]
fn terminal_state_resize_burst() {
let mut state = TerminalState::new(TerminalMode::AltScreen);
state.acknowledge_redraw();
let changed = state.handle_event(&TerminalEvent::ResizeBurst {
final_width: 200,
final_height: 50,
burst_count: 5,
});
assert!(changed);
assert_eq!(state.width, 200);
assert_eq!(state.height, 50);
assert_eq!(state.pending_resizes(), 5);
assert!(state.needs_full_redraw());
}
#[test]
fn terminal_state_acknowledge_redraw() {
let mut state = TerminalState::new(TerminalMode::AltScreen);
assert!(state.needs_full_redraw());
state.acknowledge_redraw();
assert!(!state.needs_full_redraw());
assert_eq!(state.pending_resizes(), 0);
}
#[test]
fn terminal_event_resize_equality() {
let a = TerminalEvent::Resized {
width: 80,
height: 24,
};
let b = TerminalEvent::Resized {
width: 80,
height: 24,
};
assert_eq!(a, b);
}
#[test]
fn terminal_event_reconnected() {
assert_eq!(TerminalEvent::Reconnected, TerminalEvent::Reconnected);
}
#[test]
fn terminal_state_default_impl() {
let state = TerminalState::default();
assert_eq!(state.mode, TerminalMode::AltScreen);
}
#[test]
fn terminal_state_multiple_events() {
let mut state = TerminalState::new(TerminalMode::AltScreen);
state.acknowledge_redraw();
state.handle_event(&TerminalEvent::Resized {
width: 100,
height: 30,
});
state.handle_event(&TerminalEvent::FocusLost);
state.handle_event(&TerminalEvent::Reconnected);
assert_eq!(state.width, 100);
assert_eq!(state.height, 30);
assert!(!state.focused);
assert!(state.connected);
assert!(state.needs_full_redraw());
}
}