standout_input/sources/
clipboard.rs1use std::sync::Arc;
4
5use clap::ArgMatches;
6
7use crate::collector::InputCollector;
8use crate::env::{ClipboardReader, DefaultClipboard};
9use crate::InputError;
10
11#[derive(Clone)]
42pub struct ClipboardSource<R: ClipboardReader = DefaultClipboard> {
43 reader: Arc<R>,
44 trim: bool,
45}
46
47impl ClipboardSource<DefaultClipboard> {
48 pub fn new() -> Self {
56 Self {
57 reader: Arc::new(DefaultClipboard),
58 trim: true,
59 }
60 }
61}
62
63impl Default for ClipboardSource<DefaultClipboard> {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl<R: ClipboardReader> ClipboardSource<R> {
70 pub fn with_reader(reader: R) -> Self {
74 Self {
75 reader: Arc::new(reader),
76 trim: true,
77 }
78 }
79
80 pub fn trim(mut self, trim: bool) -> Self {
84 self.trim = trim;
85 self
86 }
87}
88
89impl<R: ClipboardReader + 'static> InputCollector<String> for ClipboardSource<R> {
90 fn name(&self) -> &'static str {
91 "clipboard"
92 }
93
94 fn is_available(&self, _matches: &ArgMatches) -> bool {
95 match self.reader.read() {
97 Ok(Some(content)) => !content.trim().is_empty(),
98 Ok(None) => false,
99 Err(e) => {
100 eprintln!("Warning: clipboard unavailable: {}", e);
103 false
104 }
105 }
106 }
107
108 fn collect(&self, _matches: &ArgMatches) -> Result<Option<String>, InputError> {
109 match self.reader.read()? {
110 Some(content) => {
111 let result = if self.trim {
112 content.trim().to_string()
113 } else {
114 content
115 };
116
117 if result.is_empty() {
118 Ok(None)
119 } else {
120 Ok(Some(result))
121 }
122 }
123 None => Ok(None),
124 }
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::env::MockClipboard;
132 use clap::Command;
133
134 fn empty_matches() -> ArgMatches {
135 Command::new("test").try_get_matches_from(["test"]).unwrap()
136 }
137
138 #[test]
139 fn clipboard_available_when_has_content() {
140 let source = ClipboardSource::with_reader(MockClipboard::with_content("content"));
141 assert!(source.is_available(&empty_matches()));
142 }
143
144 #[test]
145 fn clipboard_unavailable_when_empty() {
146 let source = ClipboardSource::with_reader(MockClipboard::empty());
147 assert!(!source.is_available(&empty_matches()));
148 }
149
150 #[test]
151 fn clipboard_unavailable_when_whitespace_only() {
152 let source = ClipboardSource::with_reader(MockClipboard::with_content(" \n\t "));
153 assert!(!source.is_available(&empty_matches()));
154 }
155
156 #[test]
157 fn clipboard_collects_content() {
158 let source = ClipboardSource::with_reader(MockClipboard::with_content("hello"));
159 let result = source.collect(&empty_matches()).unwrap();
160 assert_eq!(result, Some("hello".to_string()));
161 }
162
163 #[test]
164 fn clipboard_trims_whitespace() {
165 let source = ClipboardSource::with_reader(MockClipboard::with_content(" hello \n"));
166 let result = source.collect(&empty_matches()).unwrap();
167 assert_eq!(result, Some("hello".to_string()));
168 }
169
170 #[test]
171 fn clipboard_no_trim() {
172 let source =
173 ClipboardSource::with_reader(MockClipboard::with_content(" hello ")).trim(false);
174 let result = source.collect(&empty_matches()).unwrap();
175 assert_eq!(result, Some(" hello ".to_string()));
176 }
177
178 #[test]
179 fn clipboard_returns_none_when_empty() {
180 let source = ClipboardSource::with_reader(MockClipboard::empty());
181 let result = source.collect(&empty_matches()).unwrap();
182 assert_eq!(result, None);
183 }
184}