standout_input/
input_sources.rs1use std::fmt;
2use std::sync::Arc;
3
4use crate::env::{ClipboardReader, RealClipboard, RealStdin, StdinReader};
5use crate::responder::PromptResponder;
6
7#[derive(Clone)]
8pub struct InputSources {
9 stdin: Arc<dyn StdinReader>,
10 clipboard: Arc<dyn ClipboardReader>,
11 responder: Option<Arc<dyn PromptResponder>>,
12}
13
14impl fmt::Debug for InputSources {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 f.debug_struct("InputSources")
17 .field("has_responder", &self.responder.is_some())
18 .finish_non_exhaustive()
19 }
20}
21
22impl InputSources {
23 pub fn new(
24 stdin: impl StdinReader + 'static,
25 clipboard: impl ClipboardReader + 'static,
26 responder: Option<Arc<dyn PromptResponder>>,
27 ) -> Self {
28 Self {
29 stdin: Arc::new(stdin),
30 clipboard: Arc::new(clipboard),
31 responder,
32 }
33 }
34
35 pub fn from_process() -> Self {
36 Self::new(RealStdin, RealClipboard, None)
37 }
38
39 pub fn with_stdin(mut self, stdin: impl StdinReader + 'static) -> Self {
40 self.stdin = Arc::new(stdin);
41 self
42 }
43
44 pub fn with_clipboard(mut self, clipboard: impl ClipboardReader + 'static) -> Self {
45 self.clipboard = Arc::new(clipboard);
46 self
47 }
48
49 pub fn with_responder(mut self, responder: Arc<dyn PromptResponder>) -> Self {
50 self.responder = Some(responder);
51 self
52 }
53
54 pub fn stdin(&self) -> &dyn StdinReader {
55 self.stdin.as_ref()
56 }
57
58 pub fn stdin_arc(&self) -> Arc<dyn StdinReader> {
59 Arc::clone(&self.stdin)
60 }
61
62 pub fn clipboard(&self) -> &dyn ClipboardReader {
63 self.clipboard.as_ref()
64 }
65
66 pub fn clipboard_arc(&self) -> Arc<dyn ClipboardReader> {
67 Arc::clone(&self.clipboard)
68 }
69
70 pub fn responder(&self) -> Option<&dyn PromptResponder> {
71 self.responder.as_deref()
72 }
73
74 pub fn responder_arc(&self) -> Option<Arc<dyn PromptResponder>> {
75 self.responder.clone()
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::env::{MockClipboard, MockStdin};
83
84 fn sample() -> InputSources {
85 InputSources::new(MockStdin::piped("hello"), MockClipboard::empty(), None)
86 }
87
88 #[test]
89 fn input_sources_constructs_from_explicit_readers() {
90 let sources = sample();
91 assert!(!sources.stdin().is_terminal());
92 assert_eq!(sources.stdin().read_to_string().unwrap(), "hello");
93 assert_eq!(sources.clipboard().read().unwrap(), None);
94 assert!(sources.responder().is_none());
95 }
96
97 #[test]
98 fn input_sources_is_not_copy() {
99 struct Probe<U>(std::marker::PhantomData<U>);
100 trait AmbiguousIfImpl<A> {
101 fn check() {}
102 }
103 impl<U> AmbiguousIfImpl<()> for Probe<U> {}
104 impl<U: Copy> AmbiguousIfImpl<u8> for Probe<U> {}
105 let _ = <Probe<InputSources> as AmbiguousIfImpl<_>>::check;
106
107 let sources = sample();
108 let moved = sources;
109 assert!(!moved.stdin().is_terminal());
110 }
111
112 #[test]
113 fn input_sources_is_clone() {
114 let sources = sample();
115 let cloned = sources.clone();
116 assert_eq!(cloned.stdin().read_to_string().unwrap(), "hello");
117 assert_eq!(sources.stdin().read_to_string().unwrap(), "hello");
118 }
119
120 #[test]
121 fn input_sources_debug_is_structural() {
122 let sources = sample();
123 let debug = format!("{sources:?}");
124 assert!(debug.contains("InputSources"));
125 assert!(debug.contains("has_responder: false"));
126 }
127
128 #[test]
129 fn from_process_constructs_production_sources() {
130 let sources = InputSources::from_process();
131 let _ = sources.stdin().is_terminal();
132 assert!(sources.responder().is_none());
133 }
134
135 #[test]
136 fn builders_replace_readers() {
137 let sources = InputSources::from_process()
138 .with_stdin(MockStdin::piped("in"))
139 .with_clipboard(MockClipboard::with_content("clip"));
140 assert_eq!(sources.stdin().read_to_string().unwrap(), "in");
141 assert_eq!(
142 sources.clipboard().read().unwrap(),
143 Some("clip".to_string())
144 );
145 }
146}