⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/desktop/desktop_ext/
clipboard.rs

1//! Clipboard Protocol
2//!
3//! Wayland wl_data_device compatible clipboard with MIME type negotiation,
4//! primary selection, and history.
5
6#[cfg(feature = "alloc")]
7use alloc::{collections::BTreeMap, vec::Vec};
8
9/// Errors that can occur during clipboard operations.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ClipboardError {
12    /// The clipboard is empty.
13    Empty,
14    /// The requested MIME type is not available.
15    MimeNotFound,
16    /// History is full (should not happen as we evict oldest).
17    HistoryFull,
18    /// Invalid operation for the current selection type.
19    InvalidSelection,
20    /// Data too large for clipboard.
21    DataTooLarge,
22    /// Source has been destroyed.
23    SourceDestroyed,
24}
25
26impl core::fmt::Display for ClipboardError {
27    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
28        match self {
29            Self::Empty => write!(f, "clipboard is empty"),
30            Self::MimeNotFound => write!(f, "MIME type not found"),
31            Self::HistoryFull => write!(f, "clipboard history full"),
32            Self::InvalidSelection => write!(f, "invalid selection type"),
33            Self::DataTooLarge => write!(f, "data too large"),
34            Self::SourceDestroyed => write!(f, "source destroyed"),
35        }
36    }
37}
38
39/// MIME types supported by the clipboard.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
41pub enum ClipboardMime {
42    /// Plain text (text/plain)
43    TextPlain,
44    /// UTF-8 plain text (text/plain;charset=utf-8)
45    TextPlainUtf8,
46    /// HTML (text/html)
47    TextHtml,
48    /// URI list (text/uri-list)
49    TextUriList,
50    /// PNG image (image/png)
51    ImagePng,
52    /// BMP image (image/bmp)
53    ImageBmp,
54    /// Custom/unknown MIME type (stored as hash)
55    Custom(u32),
56}
57
58impl ClipboardMime {
59    /// Return the MIME type string representation.
60    pub fn as_str(&self) -> &'static str {
61        match self {
62            Self::TextPlain => "text/plain",
63            Self::TextPlainUtf8 => "text/plain;charset=utf-8",
64            Self::TextHtml => "text/html",
65            Self::TextUriList => "text/uri-list",
66            Self::ImagePng => "image/png",
67            Self::ImageBmp => "image/bmp",
68            Self::Custom(_) => "application/octet-stream",
69        }
70    }
71
72    /// Check if this MIME type is a text type.
73    pub fn is_text(&self) -> bool {
74        matches!(
75            self,
76            Self::TextPlain | Self::TextPlainUtf8 | Self::TextHtml | Self::TextUriList
77        )
78    }
79}
80
81/// Selection type for X11-style selections.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
83pub enum SelectionType {
84    /// Standard clipboard (Ctrl+C/V).
85    #[default]
86    Clipboard,
87    /// Primary selection (mouse highlight).
88    Primary,
89}
90
91/// A single clipboard entry with data and associated MIME types.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct ClipboardEntry {
94    /// Data for each MIME type offered.
95    #[cfg(feature = "alloc")]
96    pub mime_data: BTreeMap<ClipboardMime, Vec<u8>>,
97    /// Timestamp (monotonic tick count when copied).
98    pub timestamp: u64,
99    /// Source surface ID (Wayland surface that set this data).
100    pub source_surface: u32,
101}
102
103#[cfg(feature = "alloc")]
104impl ClipboardEntry {
105    /// Create a new clipboard entry.
106    pub fn new(source_surface: u32, timestamp: u64) -> Self {
107        Self {
108            mime_data: BTreeMap::new(),
109            timestamp,
110            source_surface,
111        }
112    }
113
114    /// Add data for a MIME type.
115    pub fn set_data(&mut self, mime: ClipboardMime, data: Vec<u8>) {
116        self.mime_data.insert(mime, data);
117    }
118
119    /// Get data for a specific MIME type.
120    pub fn get_data(&self, mime: ClipboardMime) -> Option<&[u8]> {
121        self.mime_data.get(&mime).map(|v| v.as_slice())
122    }
123
124    /// Get all offered MIME types.
125    pub fn offered_mimes(&self) -> Vec<ClipboardMime> {
126        self.mime_data.keys().copied().collect()
127    }
128
129    /// Check if this entry offers a specific MIME type.
130    pub fn offers(&self, mime: ClipboardMime) -> bool {
131        self.mime_data.contains_key(&mime)
132    }
133
134    /// Total size of all data in this entry.
135    pub fn total_size(&self) -> usize {
136        self.mime_data.values().map(|v| v.len()).sum()
137    }
138}
139
140/// Maximum clipboard history entries.
141pub(crate) const CLIPBOARD_HISTORY_MAX: usize = 8;
142
143/// Maximum data size per clipboard entry (64 KB).
144pub(crate) const CLIPBOARD_MAX_DATA_SIZE: usize = 65536;
145
146/// Clipboard manager with history and primary selection support.
147#[derive(Debug)]
148#[cfg(feature = "alloc")]
149pub struct ClipboardManager {
150    /// Standard clipboard contents.
151    clipboard: Option<ClipboardEntry>,
152    /// Primary selection contents (mouse highlight).
153    primary: Option<ClipboardEntry>,
154    /// Clipboard history (most recent first).
155    history: Vec<ClipboardEntry>,
156    /// Whether clipboard history is enabled.
157    history_enabled: bool,
158    /// Monotonic timestamp counter.
159    tick: u64,
160}
161
162#[cfg(feature = "alloc")]
163impl Default for ClipboardManager {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169#[cfg(feature = "alloc")]
170impl ClipboardManager {
171    /// Create a new clipboard manager.
172    pub fn new() -> Self {
173        Self {
174            clipboard: None,
175            primary: None,
176            history: Vec::new(),
177            history_enabled: true,
178            tick: 0,
179        }
180    }
181
182    /// Copy data to the clipboard or primary selection.
183    pub fn copy(
184        &mut self,
185        selection: SelectionType,
186        source_surface: u32,
187        mime: ClipboardMime,
188        data: Vec<u8>,
189    ) -> Result<(), ClipboardError> {
190        if data.len() > CLIPBOARD_MAX_DATA_SIZE {
191            return Err(ClipboardError::DataTooLarge);
192        }
193
194        self.tick += 1;
195        let mut entry = ClipboardEntry::new(source_surface, self.tick);
196        entry.set_data(mime, data);
197
198        match selection {
199            SelectionType::Clipboard => {
200                // Push old clipboard to history if enabled.
201                if self.history_enabled {
202                    if let Some(old) = self.clipboard.take() {
203                        self.push_history(old);
204                    }
205                }
206                self.clipboard = Some(entry);
207            }
208            SelectionType::Primary => {
209                self.primary = Some(entry);
210            }
211        }
212
213        Ok(())
214    }
215
216    /// Copy data with multiple MIME representations.
217    pub fn copy_multi(
218        &mut self,
219        selection: SelectionType,
220        source_surface: u32,
221        data: &[(ClipboardMime, Vec<u8>)],
222    ) -> Result<(), ClipboardError> {
223        let total_size: usize = data.iter().map(|(_, d)| d.len()).sum();
224        if total_size > CLIPBOARD_MAX_DATA_SIZE {
225            return Err(ClipboardError::DataTooLarge);
226        }
227
228        self.tick += 1;
229        let mut entry = ClipboardEntry::new(source_surface, self.tick);
230        for (mime, d) in data {
231            entry.set_data(*mime, d.clone());
232        }
233
234        match selection {
235            SelectionType::Clipboard => {
236                if self.history_enabled {
237                    if let Some(old) = self.clipboard.take() {
238                        self.push_history(old);
239                    }
240                }
241                self.clipboard = Some(entry);
242            }
243            SelectionType::Primary => {
244                self.primary = Some(entry);
245            }
246        }
247
248        Ok(())
249    }
250
251    /// Paste data from the clipboard or primary selection.
252    pub fn paste(
253        &self,
254        selection: SelectionType,
255        mime: ClipboardMime,
256    ) -> Result<&[u8], ClipboardError> {
257        let entry = match selection {
258            SelectionType::Clipboard => self.clipboard.as_ref(),
259            SelectionType::Primary => self.primary.as_ref(),
260        };
261
262        let entry = entry.ok_or(ClipboardError::Empty)?;
263        entry.get_data(mime).ok_or(ClipboardError::MimeNotFound)
264    }
265
266    /// Get the list of MIME types available for pasting.
267    pub fn available_mimes(&self, selection: SelectionType) -> Vec<ClipboardMime> {
268        match selection {
269            SelectionType::Clipboard => self
270                .clipboard
271                .as_ref()
272                .map(|e| e.offered_mimes())
273                .unwrap_or_default(),
274            SelectionType::Primary => self
275                .primary
276                .as_ref()
277                .map(|e| e.offered_mimes())
278                .unwrap_or_default(),
279        }
280    }
281
282    /// Clear the clipboard or primary selection.
283    pub fn clear(&mut self, selection: SelectionType) {
284        match selection {
285            SelectionType::Clipboard => {
286                self.clipboard = None;
287            }
288            SelectionType::Primary => {
289                self.primary = None;
290            }
291        }
292    }
293
294    /// Get clipboard history entries.
295    pub fn history(&self) -> &[ClipboardEntry] {
296        &self.history
297    }
298
299    /// Restore a history entry to the current clipboard.
300    pub fn restore_from_history(&mut self, index: usize) -> Result<(), ClipboardError> {
301        if index >= self.history.len() {
302            return Err(ClipboardError::Empty);
303        }
304        let entry = self.history.remove(index);
305        if let Some(old) = self.clipboard.take() {
306            self.push_history(old);
307        }
308        self.clipboard = Some(entry);
309        Ok(())
310    }
311
312    /// Clear all history.
313    pub fn clear_history(&mut self) {
314        self.history.clear();
315    }
316
317    /// Enable or disable clipboard history.
318    pub fn set_history_enabled(&mut self, enabled: bool) {
319        self.history_enabled = enabled;
320        if !enabled {
321            self.history.clear();
322        }
323    }
324
325    /// Check if clipboard has data.
326    pub fn has_data(&self, selection: SelectionType) -> bool {
327        match selection {
328            SelectionType::Clipboard => self.clipboard.is_some(),
329            SelectionType::Primary => self.primary.is_some(),
330        }
331    }
332
333    /// Negotiate the best MIME type between offered types and requested types.
334    pub fn negotiate_mime(
335        &self,
336        selection: SelectionType,
337        requested: &[ClipboardMime],
338    ) -> Option<ClipboardMime> {
339        let available = self.available_mimes(selection);
340        // Return the first requested type that is available.
341        requested.iter().find(|r| available.contains(r)).copied()
342    }
343
344    /// Push an entry to history, evicting oldest if at capacity.
345    fn push_history(&mut self, entry: ClipboardEntry) {
346        if self.history.len() >= CLIPBOARD_HISTORY_MAX {
347            self.history.pop();
348        }
349        self.history.insert(0, entry);
350    }
351
352    /// Get current tick count.
353    pub fn current_tick(&self) -> u64 {
354        self.tick
355    }
356}