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