runmat_runtime/
console.rs1use once_cell::sync::OnceCell;
2use runmat_builtins::Value;
3use runmat_thread_local::runmat_thread_local;
4use runmat_time::unix_timestamp_ms;
5use std::cell::RefCell;
6use std::sync::{Arc, RwLock};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ConsoleStream {
11 Stdout,
12 Stderr,
13 ClearScreen,
14}
15
16#[derive(Clone, Debug)]
18pub struct ConsoleEntry {
19 pub stream: ConsoleStream,
20 pub text: String,
21 pub timestamp_ms: u64,
22}
23
24type StreamForwarder = dyn Fn(&ConsoleEntry) + Send + Sync + 'static;
25
26runmat_thread_local! {
27 static THREAD_BUFFER: RefCell<Vec<ConsoleEntry>> = const { RefCell::new(Vec::new()) };
28 static LAST_VALUE_OUTPUT: RefCell<Option<Value>> = const { RefCell::new(None) };
29}
30
31static FORWARDER: OnceCell<RwLock<Option<Arc<StreamForwarder>>>> = OnceCell::new();
32
33fn now_ms() -> u64 {
34 unix_timestamp_ms().min(u64::MAX as u128) as u64
35}
36
37pub fn record_console_output(stream: ConsoleStream, text: impl Into<String>) {
40 let entry = ConsoleEntry {
41 stream,
42 text: text.into(),
43 timestamp_ms: now_ms(),
44 };
45 THREAD_BUFFER.with(|buf| buf.borrow_mut().push(entry.clone()));
46
47 if let Some(forwarder) = FORWARDER
48 .get()
49 .and_then(|lock| lock.read().ok().map(|guard| guard.as_ref().cloned()))
50 .flatten()
51 {
52 forwarder(&entry);
53 }
54}
55
56pub fn record_clear_screen() {
58 record_console_output(ConsoleStream::ClearScreen, String::new());
59}
60
61pub fn record_console_line(stream: ConsoleStream, text: impl Into<String>) {
63 let mut text = text.into();
64 if !text.ends_with('\n') {
65 text.push('\n');
66 }
67 record_console_output(stream, text);
68}
69
70pub fn reset_thread_buffer() {
73 THREAD_BUFFER.with(|buf| buf.borrow_mut().clear());
74 LAST_VALUE_OUTPUT.with(|value| value.borrow_mut().take());
75}
76
77pub fn take_thread_buffer() -> Vec<ConsoleEntry> {
79 THREAD_BUFFER.with(|buf| buf.borrow_mut().drain(..).collect())
80}
81
82pub fn append_thread_buffer(entries: impl IntoIterator<Item = ConsoleEntry>) {
85 THREAD_BUFFER.with(|buf| {
86 let mut buf = buf.borrow_mut();
87 buf.extend(entries);
88 buf.sort_by_key(|entry| entry.timestamp_ms);
89 });
90}
91
92pub fn install_forwarder(forwarder: Option<Arc<StreamForwarder>>) {
95 let lock = FORWARDER.get_or_init(|| RwLock::new(None));
96 if let Ok(mut guard) = lock.write() {
97 *guard = forwarder;
98 }
99}
100
101pub fn record_value_output(label: Option<&str>, value: &Value) {
103 LAST_VALUE_OUTPUT.with(|last| {
104 *last.borrow_mut() = Some(value.clone());
105 });
106 let value_text = match value {
107 Value::Object(obj) if obj.is_class("datetime") => {
108 crate::builtins::datetime::datetime_display_text(value)
109 .ok()
110 .flatten()
111 .unwrap_or_else(|| value.to_string())
112 }
113 Value::Object(obj) if obj.is_class("duration") => {
114 crate::builtins::duration::duration_display_text(value)
115 .ok()
116 .flatten()
117 .unwrap_or_else(|| value.to_string())
118 }
119 _ => value.to_string(),
120 };
121 let text = if let Some(name) = label {
122 if is_unlabeled_nd_page_display(&value_text) {
123 inject_label_into_nd_page_headers(name, &value_text)
124 } else if value_text.contains('\n') {
125 format!("{name} =\n{value_text}")
126 } else {
127 format!("{name} = {value_text}")
128 }
129 } else {
130 value_text
131 };
132 record_console_line(ConsoleStream::Stdout, text);
133}
134
135pub fn take_last_value_output() -> Option<Value> {
136 LAST_VALUE_OUTPUT.with(|value| value.borrow_mut().take())
137}
138
139fn is_unlabeled_nd_page_display(text: &str) -> bool {
140 text.lines()
141 .any(|line| line.trim_start().starts_with("(:, :") && line.trim_end().ends_with('='))
142}
143
144fn inject_label_into_nd_page_headers(label: &str, text: &str) -> String {
145 let mut out = String::new();
146 for (idx, line) in text.lines().enumerate() {
147 if idx > 0 {
148 out.push('\n');
149 }
150 let trimmed = line.trim_start();
151 if trimmed.starts_with("(:, :") && trimmed.trim_end().ends_with('=') {
152 out.push_str(label);
153 out.push_str(trimmed);
154 } else {
155 out.push_str(line);
156 }
157 }
158 out
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 fn entry(text: &str, timestamp_ms: u64) -> ConsoleEntry {
166 ConsoleEntry {
167 stream: ConsoleStream::Stdout,
168 text: text.to_string(),
169 timestamp_ms,
170 }
171 }
172
173 #[test]
174 fn append_thread_buffer_orders_entries_by_timestamp_stably() {
175 reset_thread_buffer();
176 append_thread_buffer(vec![entry("late", 20)]);
177 append_thread_buffer(vec![entry("early", 10), entry("same-time", 20)]);
178
179 let entries = take_thread_buffer();
180 let texts = entries
181 .into_iter()
182 .map(|entry| entry.text)
183 .collect::<Vec<_>>();
184 assert_eq!(texts, vec!["early", "late", "same-time"]);
185 }
186}