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