reovim_server/session/
capture.rs1use std::{
15 collections::HashMap,
16 sync::atomic::{AtomicU64, Ordering},
17 time::Duration,
18};
19
20use {parking_lot::RwLock, tokio::sync::oneshot};
21
22pub const CAPTURE_TIMEOUT_SECS: u64 = 5;
24
25#[derive(Debug)]
27pub enum CaptureError {
28 NoTuiClient,
30 Timeout,
32 Disconnected,
34 InvalidResponse(String),
36}
37
38impl std::fmt::Display for CaptureError {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 Self::NoTuiClient => {
42 write!(f, "No TUI client connected. Start TUI first with `reovim tui`")
43 }
44 Self::Timeout => {
45 write!(f, "TUI capture timeout ({CAPTURE_TIMEOUT_SECS}s). TUI may be frozen")
46 }
47 Self::Disconnected => write!(f, "TUI client disconnected during capture"),
48 Self::InvalidResponse(msg) => write!(f, "Invalid capture response: {msg}"),
49 }
50 }
51}
52
53impl std::error::Error for CaptureError {}
54
55#[derive(Debug, Clone)]
57pub struct CaptureResult {
58 pub width: u64,
60 pub height: u64,
62 pub format: String,
64 pub content: String,
66}
67
68pub struct CaptureTracker {
73 next_id: AtomicU64,
75 pending: RwLock<HashMap<u64, PendingCapture>>,
77}
78
79struct PendingCapture {
81 sender: oneshot::Sender<CaptureResult>,
83}
84
85impl CaptureTracker {
86 #[must_use]
88 pub fn new() -> Self {
89 Self {
90 next_id: AtomicU64::new(1),
91 pending: RwLock::new(HashMap::new()),
92 }
93 }
94
95 pub fn create_pending(&self) -> (u64, oneshot::Receiver<CaptureResult>) {
99 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
100 let (tx, rx) = oneshot::channel();
101 self.pending
102 .write()
103 .insert(id, PendingCapture { sender: tx });
104 (id, rx)
105 }
106
107 pub fn deliver_response(&self, request_id: u64, result: CaptureResult) -> bool {
111 let pending = self.pending.write().remove(&request_id);
113 if let Some(pending) = pending {
114 let _ = pending.sender.send(result);
116 true
117 } else {
118 tracing::warn!("Received capture response for unknown request {request_id}");
119 false
120 }
121 }
122
123 pub fn cancel(&self, request_id: u64) -> bool {
127 self.pending.write().remove(&request_id).is_some()
128 }
129
130 #[must_use]
132 pub fn pending_count(&self) -> usize {
133 self.pending.read().len()
134 }
135}
136
137impl Default for CaptureTracker {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143impl std::fmt::Debug for CaptureTracker {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 f.debug_struct("CaptureTracker")
146 .field("pending_count", &self.pending_count())
147 .finish()
148 }
149}
150
151pub async fn wait_for_capture(
162 rx: oneshot::Receiver<CaptureResult>,
163) -> Result<CaptureResult, CaptureError> {
164 match tokio::time::timeout(Duration::from_secs(CAPTURE_TIMEOUT_SECS), rx).await {
165 Ok(Ok(result)) => Ok(result),
166 Ok(Err(_)) => Err(CaptureError::Disconnected),
167 Err(_) => Err(CaptureError::Timeout),
168 }
169}
170
171#[cfg(test)]
172#[path = "capture_tests.rs"]
173mod tests;