rust_expect/interact/
hooks.rs

1//! Interaction hooks for customizing behavior.
2
3use std::sync::Arc;
4
5/// Hook type for input processing.
6pub type InputHook = Arc<dyn Fn(&[u8]) -> Vec<u8> + Send + Sync>;
7
8/// Hook type for output processing.
9pub type OutputHook = Arc<dyn Fn(&[u8]) -> Vec<u8> + Send + Sync>;
10
11/// Hook type for events.
12pub type EventHook = Arc<dyn Fn(InteractionEvent) + Send + Sync>;
13
14/// Interaction events.
15#[derive(Debug, Clone)]
16pub enum InteractionEvent {
17    /// Session started.
18    Started,
19    /// Session ended.
20    Ended,
21    /// Input received from user.
22    Input(Vec<u8>),
23    /// Output received from session.
24    Output(Vec<u8>),
25    /// Exit character pressed.
26    ExitRequested,
27    /// Escape sequence detected.
28    EscapeSequence(Vec<u8>),
29    /// Window resized.
30    Resize {
31        /// New column count.
32        cols: u16,
33        /// New row count.
34        rows: u16,
35    },
36}
37
38/// Hook manager for interaction sessions.
39#[derive(Default)]
40pub struct HookManager {
41    /// Input processing hooks.
42    inputs: Vec<InputHook>,
43    /// Output processing hooks.
44    outputs: Vec<OutputHook>,
45    /// Event notification hooks.
46    events: Vec<EventHook>,
47}
48
49impl HookManager {
50    /// Create a new hook manager.
51    #[must_use]
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Add an input processing hook.
57    pub fn add_input_hook<F>(&mut self, hook: F)
58    where
59        F: Fn(&[u8]) -> Vec<u8> + Send + Sync + 'static,
60    {
61        self.inputs.push(Arc::new(hook));
62    }
63
64    /// Add an output processing hook.
65    pub fn add_output_hook<F>(&mut self, hook: F)
66    where
67        F: Fn(&[u8]) -> Vec<u8> + Send + Sync + 'static,
68    {
69        self.outputs.push(Arc::new(hook));
70    }
71
72    /// Add an event notification hook.
73    pub fn add_event_hook<F>(&mut self, hook: F)
74    where
75        F: Fn(InteractionEvent) + Send + Sync + 'static,
76    {
77        self.events.push(Arc::new(hook));
78    }
79
80    /// Process input through all hooks.
81    #[must_use]
82    pub fn process_input(&self, mut data: Vec<u8>) -> Vec<u8> {
83        for hook in &self.inputs {
84            data = hook(&data);
85        }
86        data
87    }
88
89    /// Process output through all hooks.
90    #[must_use]
91    pub fn process_output(&self, mut data: Vec<u8>) -> Vec<u8> {
92        for hook in &self.outputs {
93            data = hook(&data);
94        }
95        data
96    }
97
98    /// Notify all event hooks.
99    pub fn notify(&self, event: &InteractionEvent) {
100        for hook in &self.events {
101            hook(event.clone());
102        }
103    }
104
105    /// Clear all hooks.
106    pub fn clear(&mut self) {
107        self.inputs.clear();
108        self.outputs.clear();
109        self.events.clear();
110    }
111}
112
113impl std::fmt::Debug for HookManager {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        f.debug_struct("HookManager")
116            .field("inputs", &self.inputs.len())
117            .field("outputs", &self.outputs.len())
118            .field("events", &self.events.len())
119            .finish()
120    }
121}
122
123/// Builder for creating hook chains.
124#[derive(Default)]
125pub struct HookBuilder {
126    manager: HookManager,
127}
128
129impl HookBuilder {
130    /// Create a new builder.
131    #[must_use]
132    pub fn new() -> Self {
133        Self::default()
134    }
135
136    /// Add CRLF translation hook.
137    #[must_use]
138    pub fn with_crlf(mut self) -> Self {
139        self.manager.add_input_hook(|data| {
140            let mut result = Vec::with_capacity(data.len() * 2);
141            for &b in data {
142                if b == b'\n' {
143                    result.push(b'\r');
144                    result.push(b'\n');
145                } else {
146                    result.push(b);
147                }
148            }
149            result
150        });
151        self
152    }
153
154    /// Add local echo hook.
155    #[must_use]
156    pub fn with_echo(mut self) -> Self {
157        self.manager.add_input_hook(|data| {
158            // Echo to stdout
159            let _ = std::io::Write::write_all(&mut std::io::stdout(), data);
160            let _ = std::io::Write::flush(&mut std::io::stdout());
161            data.to_vec()
162        });
163        self
164    }
165
166    /// Add logging hook.
167    #[must_use]
168    pub fn with_logging(mut self) -> Self {
169        self.manager.add_event_hook(|event| match event {
170            InteractionEvent::Started => eprintln!("[interact] Session started"),
171            InteractionEvent::Ended => eprintln!("[interact] Session ended"),
172            InteractionEvent::ExitRequested => eprintln!("[interact] Exit requested"),
173            _ => {}
174        });
175        self
176    }
177
178    /// Build the hook manager.
179    #[must_use]
180    pub fn build(self) -> HookManager {
181        self.manager
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn hook_manager_process_input() {
191        let mut manager = HookManager::new();
192        manager.add_input_hook(|data| data.iter().map(u8::to_ascii_uppercase).collect());
193
194        let result = manager.process_input(b"hello".to_vec());
195        assert_eq!(result, b"HELLO");
196    }
197
198    #[test]
199    fn hook_chain() {
200        let mut manager = HookManager::new();
201        manager.add_input_hook(|data| {
202            let mut v = data.to_vec();
203            v.push(b'1');
204            v
205        });
206        manager.add_input_hook(|data| {
207            let mut v = data.to_vec();
208            v.push(b'2');
209            v
210        });
211
212        let result = manager.process_input(b"x".to_vec());
213        assert_eq!(result, b"x12");
214    }
215
216    #[test]
217    fn hook_builder() {
218        let manager = HookBuilder::new().with_crlf().build();
219
220        let result = manager.process_input(b"a\nb".to_vec());
221        assert_eq!(result, b"a\r\nb");
222    }
223}