standout_input/sources/
clipboard.rs1use std::sync::Arc;
2
3use clap::ArgMatches;
4
5use crate::collector::InputCollector;
6use crate::env::{ClipboardReader, RealClipboard};
7use crate::InputError;
8use crate::InputSources;
9
10#[derive(Clone)]
11pub struct ClipboardSource {
12 reader: Option<Arc<dyn ClipboardReader>>,
13 trim: bool,
14}
15
16impl ClipboardSource {
17 pub fn new() -> Self {
18 Self {
19 reader: None,
20 trim: true,
21 }
22 }
23
24 pub fn with_reader(reader: impl ClipboardReader + 'static) -> Self {
25 Self {
26 reader: Some(Arc::new(reader)),
27 trim: true,
28 }
29 }
30
31 pub fn with_shared_reader(reader: Arc<dyn ClipboardReader>) -> Self {
32 Self {
33 reader: Some(reader),
34 trim: true,
35 }
36 }
37
38 pub fn trim(mut self, trim: bool) -> Self {
39 self.trim = trim;
40 self
41 }
42
43 fn reader(&self) -> &dyn ClipboardReader {
44 self.reader
45 .as_deref()
46 .unwrap_or(&RealClipboard as &dyn ClipboardReader)
47 }
48}
49
50impl Default for ClipboardSource {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl InputCollector<String> for ClipboardSource {
57 fn name(&self) -> &'static str {
58 "clipboard"
59 }
60
61 fn is_available(&self, _matches: &ArgMatches) -> bool {
62 match self.reader().read() {
63 Ok(Some(content)) => !content.trim().is_empty(),
64 Ok(None) => false,
65 Err(e) => {
66 eprintln!("Warning: clipboard unavailable: {}", e);
67 false
68 }
69 }
70 }
71
72 fn collect(&self, _matches: &ArgMatches) -> Result<Option<String>, InputError> {
73 match self.reader().read()? {
74 Some(content) => {
75 let result = if self.trim {
76 content.trim().to_string()
77 } else {
78 content
79 };
80
81 if result.is_empty() {
82 Ok(None)
83 } else {
84 Ok(Some(result))
85 }
86 }
87 None => Ok(None),
88 }
89 }
90
91 fn bind_sources(&self, sources: &InputSources) -> Option<Box<dyn InputCollector<String>>> {
92 if self.reader.is_some() {
93 return None;
94 }
95 Some(Box::new(Self {
96 reader: Some(sources.clipboard_arc()),
97 trim: self.trim,
98 }))
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::env::MockClipboard;
106 use clap::Command;
107
108 fn empty_matches() -> ArgMatches {
109 Command::new("test").try_get_matches_from(["test"]).unwrap()
110 }
111
112 #[test]
113 fn clipboard_available_when_has_content() {
114 let source = ClipboardSource::with_reader(MockClipboard::with_content("content"));
115 assert!(source.is_available(&empty_matches()));
116 }
117
118 #[test]
119 fn clipboard_unavailable_when_empty() {
120 let source = ClipboardSource::with_reader(MockClipboard::empty());
121 assert!(!source.is_available(&empty_matches()));
122 }
123
124 #[test]
125 fn clipboard_unavailable_when_whitespace_only() {
126 let source = ClipboardSource::with_reader(MockClipboard::with_content(" \n\t "));
127 assert!(!source.is_available(&empty_matches()));
128 }
129
130 #[test]
131 fn clipboard_collects_content() {
132 let source = ClipboardSource::with_reader(MockClipboard::with_content("hello"));
133 let result = source.collect(&empty_matches()).unwrap();
134 assert_eq!(result, Some("hello".to_string()));
135 }
136
137 #[test]
138 fn clipboard_trims_whitespace() {
139 let source = ClipboardSource::with_reader(MockClipboard::with_content(" hello \n"));
140 let result = source.collect(&empty_matches()).unwrap();
141 assert_eq!(result, Some("hello".to_string()));
142 }
143
144 #[test]
145 fn clipboard_no_trim() {
146 let source =
147 ClipboardSource::with_reader(MockClipboard::with_content(" hello ")).trim(false);
148 let result = source.collect(&empty_matches()).unwrap();
149 assert_eq!(result, Some(" hello ".to_string()));
150 }
151
152 #[test]
153 fn clipboard_returns_none_when_empty() {
154 let source = ClipboardSource::with_reader(MockClipboard::empty());
155 let result = source.collect(&empty_matches()).unwrap();
156 assert_eq!(result, None);
157 }
158
159 #[test]
160 fn bind_sources_uses_invocation_clipboard() {
161 let source = ClipboardSource::new();
162 let sources =
163 InputSources::from_process().with_clipboard(MockClipboard::with_content("bound"));
164 let bound = source.bind_sources(&sources).expect("unbound source binds");
165 let result = bound.collect(&empty_matches()).unwrap();
166 assert_eq!(result, Some("bound".to_string()));
167 }
168}