runmat_runtime/
interaction.rs1use once_cell::sync::OnceCell;
2use runmat_thread_local::runmat_thread_local;
3use std::cell::RefCell;
4use std::future::Future;
5use std::pin::Pin;
6
7use crate::{build_runtime_error, RuntimeError};
8#[cfg(not(target_arch = "wasm32"))]
9use std::io::IsTerminal;
10#[cfg(not(target_arch = "wasm32"))]
11use std::io::{self, Read, Write};
12#[cfg(all(feature = "interaction-test-hooks", not(target_arch = "wasm32")))]
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::{Arc, RwLock};
15
16pub use runmat_async::InteractionKind;
17
18#[derive(Clone)]
19pub struct InteractionPromptOwned {
20 pub prompt: String,
21 pub kind: InteractionKind,
22}
23
24#[derive(Clone)]
25pub enum InteractionResponse {
26 Line(String),
27 KeyPress,
28}
29
30pub type AsyncInteractionFuture =
31 Pin<Box<dyn Future<Output = Result<InteractionResponse, String>> + 'static>>;
32
33pub type AsyncInteractionHandler =
34 dyn Fn(InteractionPromptOwned) -> AsyncInteractionFuture + Send + Sync;
35
36static ASYNC_HANDLER: OnceCell<RwLock<Option<Arc<AsyncInteractionHandler>>>> = OnceCell::new();
37runmat_thread_local! {
38 static QUEUED_RESPONSE: RefCell<Option<Result<InteractionResponse, String>>> =
39 const { RefCell::new(None) };
40}
41
42#[cfg(all(feature = "interaction-test-hooks", not(target_arch = "wasm32")))]
43static FORCE_INTERACTIVE_STDIN: AtomicBool = AtomicBool::new(false);
44
45#[cfg(all(feature = "interaction-test-hooks", not(target_arch = "wasm32")))]
46pub fn force_interactive_stdin_for_tests(enable: bool) {
47 FORCE_INTERACTIVE_STDIN.store(enable, Ordering::Relaxed);
48}
49
50#[cfg(all(not(feature = "interaction-test-hooks"), not(target_arch = "wasm32")))]
51#[inline]
52fn force_interactive_stdin() -> bool {
53 false
54}
55
56#[cfg(all(feature = "interaction-test-hooks", not(target_arch = "wasm32")))]
57#[inline]
58fn force_interactive_stdin() -> bool {
59 FORCE_INTERACTIVE_STDIN.load(Ordering::Relaxed)
60}
61
62fn async_handler_slot() -> &'static RwLock<Option<Arc<AsyncInteractionHandler>>> {
63 ASYNC_HANDLER.get_or_init(|| RwLock::new(None))
64}
65
66fn interaction_error(identifier: &str, message: impl Into<String>) -> RuntimeError {
67 build_runtime_error(message)
68 .with_identifier(identifier.to_string())
69 .build()
70}
71
72pub struct AsyncHandlerGuard {
73 previous: Option<Arc<AsyncInteractionHandler>>,
74}
75
76impl Drop for AsyncHandlerGuard {
77 fn drop(&mut self) {
78 let mut slot = async_handler_slot()
79 .write()
80 .unwrap_or_else(|_| panic!("interaction async handler lock poisoned"));
81 *slot = self.previous.take();
82 }
83}
84
85pub fn replace_async_handler(handler: Option<Arc<AsyncInteractionHandler>>) -> AsyncHandlerGuard {
86 let mut slot = async_handler_slot()
87 .write()
88 .unwrap_or_else(|_| panic!("interaction async handler lock poisoned"));
89 let previous = std::mem::replace(&mut *slot, handler);
90 AsyncHandlerGuard { previous }
91}
92
93pub async fn request_line_async(prompt: &str, echo: bool) -> Result<String, RuntimeError> {
94 if let Some(response) = QUEUED_RESPONSE.with(|slot| slot.borrow_mut().take()) {
95 return match response
96 .map_err(|err| interaction_error("RunMat:interaction:QueuedResponseError", err))?
97 {
98 InteractionResponse::Line(value) => Ok(value),
99 InteractionResponse::KeyPress => Err(interaction_error(
100 "RunMat:interaction:UnexpectedQueuedKeypress",
101 "queued keypress response used for line request",
102 )),
103 };
104 }
105
106 if let Some(handler) = async_handler_slot()
107 .read()
108 .ok()
109 .and_then(|slot| slot.clone())
110 {
111 let owned = InteractionPromptOwned {
112 prompt: prompt.to_string(),
113 kind: InteractionKind::Line { echo },
114 };
115 let value = handler(owned)
116 .await
117 .map_err(|err| interaction_error("RunMat:interaction:AsyncHandlerError", err))?;
118 return match value {
119 InteractionResponse::Line(line) => Ok(line),
120 InteractionResponse::KeyPress => Err(interaction_error(
121 "RunMat:interaction:UnexpectedAsyncKeypress",
122 "interaction async handler returned keypress for line request",
123 )),
124 };
125 }
126
127 default_read_line(prompt, echo)
128 .map_err(|err| interaction_error("RunMat:interaction:ReadLineFailed", err))
129}
130
131pub async fn wait_for_key_async(prompt: &str) -> Result<(), RuntimeError> {
132 if let Some(response) = QUEUED_RESPONSE.with(|slot| slot.borrow_mut().take()) {
133 return match response
134 .map_err(|err| interaction_error("RunMat:interaction:QueuedResponseError", err))?
135 {
136 InteractionResponse::Line(_) => Err(interaction_error(
137 "RunMat:interaction:UnexpectedQueuedLine",
138 "queued line response used for keypress request",
139 )),
140 InteractionResponse::KeyPress => Ok(()),
141 };
142 }
143
144 if let Some(handler) = async_handler_slot()
145 .read()
146 .ok()
147 .and_then(|slot| slot.clone())
148 {
149 let owned = InteractionPromptOwned {
150 prompt: prompt.to_string(),
151 kind: InteractionKind::KeyPress,
152 };
153 let value = handler(owned)
154 .await
155 .map_err(|err| interaction_error("RunMat:interaction:AsyncHandlerError", err))?;
156 return match value {
157 InteractionResponse::Line(_) => Err(interaction_error(
158 "RunMat:interaction:UnexpectedAsyncLine",
159 "interaction async handler returned line value for keypress request",
160 )),
161 InteractionResponse::KeyPress => Ok(()),
162 };
163 }
164
165 default_wait_for_key(prompt)
166 .map_err(|err| interaction_error("RunMat:interaction:WaitForKeyFailed", err))
167}
168
169pub fn default_read_line(prompt: &str, echo: bool) -> Result<String, String> {
170 #[cfg(target_arch = "wasm32")]
171 {
172 let _ = (prompt, echo);
173 Err("stdin input is not available on wasm targets".to_string())
174 }
175 #[cfg(not(target_arch = "wasm32"))]
176 {
177 if !prompt.is_empty() {
178 let mut stdout = io::stdout();
179 write!(stdout, "{prompt}")
180 .map_err(|err| format!("input: failed to write prompt ({err})"))?;
181 stdout
182 .flush()
183 .map_err(|err| format!("input: failed to flush stdout ({err})"))?;
184 }
185 let mut line = String::new();
186 let stdin = io::stdin();
187 stdin
188 .read_line(&mut line)
189 .map_err(|err| format!("input: failed to read from stdin ({err})"))?;
190 if !echo {
191 }
193 Ok(line.trim_end_matches(&['\r', '\n'][..]).to_string())
194 }
195}
196
197pub fn default_wait_for_key(prompt: &str) -> Result<(), String> {
198 #[cfg(target_arch = "wasm32")]
199 {
200 let _ = prompt;
201 Err("keypress input is not available on wasm targets".to_string())
202 }
203 #[cfg(not(target_arch = "wasm32"))]
204 {
205 if !prompt.is_empty() {
206 let mut stdout = io::stdout();
207 write!(stdout, "{prompt}")
208 .map_err(|err| format!("pause: failed to write prompt ({err})"))?;
209 stdout
210 .flush()
211 .map_err(|err| format!("pause: failed to flush stdout ({err})"))?;
212 }
213 let stdin = io::stdin();
214 if !stdin.is_terminal() && !force_interactive_stdin() {
215 return Ok(());
216 }
217 let mut handle = stdin.lock();
218 let mut buf = [0u8; 1];
219 handle
220 .read(&mut buf)
221 .map_err(|err| format!("pause: failed to read from stdin ({err})"))?;
222 Ok(())
223 }
224}
225
226pub fn push_queued_response(response: Result<InteractionResponse, String>) {
227 QUEUED_RESPONSE.with(|slot| {
228 *slot.borrow_mut() = Some(response);
229 });
230}
231
232