murmur_core/context/
system_state.rs1use std::collections::VecDeque;
2use std::sync::{Arc, Mutex};
3
4const MAX_CLIPBOARD_CHARS: usize = 500;
6
7const DEFAULT_MAX_RECENT_CHARS: usize = 1000;
9
10const MAX_ENTRIES: usize = 50;
12
13pub struct ClipboardWatcher;
19
20impl Default for ClipboardWatcher {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl ClipboardWatcher {
27 pub fn new() -> Self {
28 ClipboardWatcher
29 }
30
31 pub fn get_clipboard_text(&self) -> Option<String> {
34 match arboard::Clipboard::new() {
35 Ok(mut cb) => match cb.get_text() {
36 Ok(text) if !text.trim().is_empty() => {
37 let trimmed = text.trim();
38 if trimmed.chars().count() > MAX_CLIPBOARD_CHARS {
39 Some(trimmed.chars().take(MAX_CLIPBOARD_CHARS).collect())
40 } else {
41 Some(trimmed.to_string())
42 }
43 }
44 _ => None,
45 },
46 Err(e) => {
47 log::debug!("Failed to access clipboard: {e}");
48 None
49 }
50 }
51 }
52}
53
54pub struct RecentTextTracker {
60 buffer: Arc<Mutex<RecentTextBuffer>>,
61}
62
63struct RecentTextBuffer {
64 entries: VecDeque<String>,
66 max_chars: usize,
68}
69
70impl Default for RecentTextTracker {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl RecentTextTracker {
77 pub fn new() -> Self {
78 Self {
79 buffer: Arc::new(Mutex::new(RecentTextBuffer {
80 entries: VecDeque::new(),
81 max_chars: DEFAULT_MAX_RECENT_CHARS,
82 })),
83 }
84 }
85
86 pub fn push(&self, text: &str) {
88 let trimmed = text.trim();
89 if trimmed.is_empty() {
90 return;
91 }
92 if let Ok(mut buf) = self.buffer.lock() {
93 buf.entries.push_back(trimmed.to_string());
94 while buf.entries.len() > MAX_ENTRIES {
95 buf.entries.pop_front();
96 }
97 let mut total_chars: usize = buf.entries.iter().map(|e| e.len()).sum();
98 while total_chars > buf.max_chars && !buf.entries.is_empty() {
99 if let Some(front) = buf.entries.pop_front() {
100 total_chars -= front.len();
101 }
102 }
103 }
104 }
105
106 pub fn get_recent_text(&self) -> Option<String> {
109 if let Ok(buf) = self.buffer.lock() {
110 if buf.entries.is_empty() {
111 return None;
112 }
113 Some(buf.entries.iter().cloned().collect::<Vec<_>>().join(" "))
114 } else {
115 None
116 }
117 }
118
119 pub fn clear(&self) {
121 if let Ok(mut buf) = self.buffer.lock() {
122 buf.entries.clear();
123 }
124 }
125
126 pub fn entry_count(&self) -> usize {
128 self.buffer.lock().map(|buf| buf.entries.len()).unwrap_or(0)
129 }
130
131 pub fn shared(&self) -> Self {
134 Self {
135 buffer: Arc::clone(&self.buffer),
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
147 fn test_clipboard_watcher_new() {
148 let watcher = ClipboardWatcher::new();
149 let _ = watcher;
150 }
151
152 #[test]
153 fn test_clipboard_watcher_returns_option() {
154 let watcher = ClipboardWatcher::new();
155 let result = watcher.get_clipboard_text();
157 let _ = result; }
159
160 #[test]
161 fn test_max_clipboard_chars_positive() {
162 const { assert!(MAX_CLIPBOARD_CHARS > 0) };
163 }
164
165 #[test]
168 fn test_recent_text_tracker_new_empty() {
169 let tracker = RecentTextTracker::new();
170 assert!(tracker.get_recent_text().is_none());
171 assert_eq!(tracker.entry_count(), 0);
172 }
173
174 #[test]
175 fn test_recent_text_tracker_push_and_get() {
176 let tracker = RecentTextTracker::new();
177 tracker.push("hello world");
178 assert_eq!(tracker.entry_count(), 1);
179 let text = tracker.get_recent_text().unwrap();
180 assert_eq!(text, "hello world");
181 }
182
183 #[test]
184 fn test_recent_text_tracker_multiple_entries() {
185 let tracker = RecentTextTracker::new();
186 tracker.push("first");
187 tracker.push("second");
188 tracker.push("third");
189 assert_eq!(tracker.entry_count(), 3);
190 let text = tracker.get_recent_text().unwrap();
191 assert!(text.contains("first"));
192 assert!(text.contains("second"));
193 assert!(text.contains("third"));
194 }
195
196 #[test]
197 fn test_recent_text_tracker_ignores_empty() {
198 let tracker = RecentTextTracker::new();
199 tracker.push("");
200 tracker.push(" ");
201 tracker.push("\n\t");
202 assert_eq!(tracker.entry_count(), 0);
203 assert!(tracker.get_recent_text().is_none());
204 }
205
206 #[test]
207 fn test_recent_text_tracker_trims_whitespace() {
208 let tracker = RecentTextTracker::new();
209 tracker.push(" hello ");
210 let text = tracker.get_recent_text().unwrap();
211 assert_eq!(text, "hello");
212 }
213
214 #[test]
215 fn test_recent_text_tracker_max_entries() {
216 let tracker = RecentTextTracker::new();
217 for i in 0..(MAX_ENTRIES + 10) {
218 tracker.push(&format!("entry {i}"));
219 }
220 assert!(tracker.entry_count() <= MAX_ENTRIES);
221 }
222
223 #[test]
224 fn test_recent_text_tracker_max_chars() {
225 let tracker = RecentTextTracker::new();
226 for _ in 0..100 {
228 tracker.push(&"a".repeat(100));
229 }
230 let text = tracker.get_recent_text().unwrap();
231 assert!(text.len() <= DEFAULT_MAX_RECENT_CHARS + 200); }
233
234 #[test]
235 fn test_recent_text_tracker_clear() {
236 let tracker = RecentTextTracker::new();
237 tracker.push("hello");
238 tracker.push("world");
239 assert_eq!(tracker.entry_count(), 2);
240 tracker.clear();
241 assert_eq!(tracker.entry_count(), 0);
242 assert!(tracker.get_recent_text().is_none());
243 }
244
245 #[test]
246 fn test_recent_text_tracker_shared() {
247 let tracker = RecentTextTracker::new();
248 let shared = tracker.shared();
249
250 tracker.push("from original");
251 assert_eq!(shared.entry_count(), 1);
252 assert_eq!(shared.get_recent_text().unwrap(), "from original");
253
254 shared.push("from shared");
255 assert_eq!(tracker.entry_count(), 2);
256 }
257
258 #[test]
259 fn test_recent_text_tracker_preserves_order() {
260 let tracker = RecentTextTracker::new();
261 tracker.push("alpha");
262 tracker.push("beta");
263 tracker.push("gamma");
264 let text = tracker.get_recent_text().unwrap();
265 let alpha_pos = text.find("alpha").unwrap();
266 let beta_pos = text.find("beta").unwrap();
267 let gamma_pos = text.find("gamma").unwrap();
268 assert!(alpha_pos < beta_pos);
269 assert!(beta_pos < gamma_pos);
270 }
271
272 #[test]
273 fn test_recent_text_tracker_thread_safe() {
274 let tracker = RecentTextTracker::new();
275 let shared = tracker.shared();
276
277 let handle = std::thread::spawn(move || {
278 for i in 0..10 {
279 shared.push(&format!("thread entry {i}"));
280 }
281 });
282
283 for i in 0..10 {
284 tracker.push(&format!("main entry {i}"));
285 }
286
287 handle.join().unwrap();
288 assert!(tracker.entry_count() > 0);
289 assert!(tracker.entry_count() <= 20);
290 }
291
292 #[test]
295 fn test_constants() {
296 const { assert!(DEFAULT_MAX_RECENT_CHARS > 0) };
297 const { assert!(MAX_ENTRIES > 0) };
298 const { assert!(MAX_CLIPBOARD_CHARS > 0) };
299 }
300}