1use endbasic_core::exec::{Machine, StopReason};
19use endbasic_std::console::{self, Console, is_narrow, refill_and_print};
20use endbasic_std::program::{BREAK_MSG, Program, continue_if_modified};
21use endbasic_std::storage::Storage;
22use std::cell::RefCell;
23use std::io;
24use std::rc::Rc;
25
26pub mod demos;
27pub mod editor;
28
29pub fn print_welcome(console: Rc<RefCell<dyn Console>>) -> io::Result<()> {
31 let mut console = console.borrow_mut();
32
33 if is_narrow(&*console) {
34 console.print(&format!("EndBASIC {}", env!("CARGO_PKG_VERSION")))?;
35 } else {
36 console.print("")?;
37 console.print(&format!(" EndBASIC {}", env!("CARGO_PKG_VERSION")))?;
38 console.print(" Copyright 2020-2026 Julio Merino")?;
39 console.print("")?;
40 console.print(" Type HELP for interactive usage information.")?;
41 }
42 console.print("")?;
43
44 Ok(())
45}
46
47pub async fn try_load_autoexec(
52 machine: &mut Machine,
53 console: Rc<RefCell<dyn Console>>,
54 storage: Rc<RefCell<Storage>>,
55) -> io::Result<()> {
56 let code = match storage.borrow().get("AUTOEXEC.BAS").await {
57 Ok(code) => code,
58 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
59 Err(e) => {
60 return console
61 .borrow_mut()
62 .print(&format!("AUTOEXEC.BAS exists but cannot be read: {}", e));
63 }
64 };
65
66 match machine.exec(&mut code.as_slice()).await {
67 Ok(_) => Ok(()),
68 Err(e) => {
69 console.borrow_mut().print(&format!("AUTOEXEC.BAS failed: {}", e))?;
70 Ok(())
71 }
72 }
73}
74
75pub async fn run_from_cloud(
78 machine: &mut Machine,
79 console: Rc<RefCell<dyn Console>>,
80 storage: Rc<RefCell<Storage>>,
81 program: Rc<RefCell<dyn Program>>,
82 username_path: &str,
83 will_run_repl: bool,
84) -> io::Result<i32> {
85 let (fs_uri, path) = match username_path.split_once('/') {
86 Some((username, path)) => (format!("cloud://{}", username), format!("AUTORUN:/{}", path)),
87 None => {
88 let mut console = console.borrow_mut();
89 console.print(&format!(
90 "Invalid program to run '{}'; must be of the form 'username/path'",
91 username_path
92 ))?;
93 return Ok(1);
94 }
95 };
96
97 console.borrow_mut().print(&format!("Mounting {} as AUTORUN...", fs_uri))?;
98 storage.borrow_mut().mount("AUTORUN", &fs_uri)?;
99 storage.borrow_mut().cd("AUTORUN:/")?;
100
101 console.borrow_mut().print(&format!("Loading {}...", path))?;
102 let content = storage.borrow().get(&path).await?;
103 let content = match String::from_utf8(content) {
104 Ok(text) => text,
105 Err(e) => {
106 let mut console = console.borrow_mut();
107 console.print(&format!("Invalid program to run '{}': {}", path, e))?;
108 return Ok(1);
109 }
110 };
111 program.borrow_mut().load(Some(&path), &content);
112
113 console.borrow_mut().print("Starting...")?;
114 console.borrow_mut().print("")?;
115
116 let result = machine.exec(&mut "RUN".as_bytes()).await;
117
118 let mut console = console.borrow_mut();
119
120 console.print("")?;
121 let code = match result {
122 Ok(r @ StopReason::Eof) => {
123 console.print("**** Program exited due to EOF ****")?;
124 r.as_exit_code()
125 }
126 Ok(r @ StopReason::Exited(_)) => {
127 let code = r.as_exit_code();
128 console.print(&format!("**** Program exited with code {} ****", code))?;
129 code
130 }
131 Ok(r @ StopReason::Break) => {
132 console.print("**** Program stopped due to BREAK ****")?;
133 r.as_exit_code()
134 }
135 Err(e) => {
136 console.print(&format!("**** ERROR: {} ****", e))?;
137 1
138 }
139 };
140
141 if will_run_repl {
142 console.print("")?;
143 refill_and_print(
144 &mut *console,
145 [
146 "You are now being dropped into the EndBASIC interpreter.",
147 "The program you asked to run is still loaded in memory and you can interact with \
148it now. Use LIST to view the source code, EDIT to launch an editor on the source code, and RUN to \
149execute the program again.",
150 "Type HELP for interactive usage information.",
151 ],
152 " ",
153 )?;
154 console.print("")?;
155 }
156
157 Ok(code)
158}
159
160pub async fn run_repl_loop(
165 machine: &mut Machine,
166 console: Rc<RefCell<dyn Console>>,
167 program: Rc<RefCell<dyn Program>>,
168) -> io::Result<i32> {
169 let mut stop_reason = StopReason::Eof;
170 let mut history = vec![];
171 while stop_reason == StopReason::Eof {
172 let line = {
173 let mut console = console.borrow_mut();
174 if console.is_interactive() {
175 console.print("Ready")?;
176 }
177 console::read_line(&mut *console, "", "", Some(&mut history)).await
178 };
179
180 machine.drain_signals();
183
184 match line {
185 Ok(line) => match machine.exec(&mut line.as_bytes()).await {
186 Ok(reason) => stop_reason = reason,
187 Err(e) => {
188 let mut console = console.borrow_mut();
189 console.print(format!("ERROR: {}", e).as_str())?;
190 }
191 },
192 Err(e) => {
193 if e.kind() == io::ErrorKind::Interrupted {
194 let mut console = console.borrow_mut();
195 console.print(BREAK_MSG)?;
196 } else if e.kind() == io::ErrorKind::UnexpectedEof {
200 let mut console = console.borrow_mut();
201 console.print("End of input by CTRL-D")?;
202 stop_reason = StopReason::Exited(0);
203 } else {
204 stop_reason = StopReason::Exited(1);
205 }
206 }
207 }
208
209 match stop_reason {
210 StopReason::Eof => (),
211 StopReason::Break => {
212 console.borrow_mut().print("**** BREAK ****")?;
213 stop_reason = StopReason::Eof;
214 }
215 StopReason::Exited(_) => {
216 if !continue_if_modified(&*program.borrow(), &mut *console.borrow_mut()).await? {
217 console.borrow_mut().print("Exit aborted; resuming REPL loop.")?;
218 stop_reason = StopReason::Eof;
219 }
220 }
221 }
222 }
223 Ok(stop_reason.as_exit_code())
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use endbasic_core::exec::Signal;
230 use endbasic_std::console::{CharsXY, Key};
231 use endbasic_std::storage::{Drive, DriveFactory, InMemoryDrive};
232 use endbasic_std::testutils::*;
233 use futures_lite::future::block_on;
234 use std::convert::TryFrom;
235
236 fn check_is_narrow_welcome(console_width: u16) -> (bool, usize) {
240 let console = Rc::from(RefCell::from(MockConsole::default()));
241 console.borrow_mut().set_size_chars(CharsXY::new(console_width, 1));
242 print_welcome(console.clone()).unwrap();
243
244 let mut console = console.borrow_mut();
245 let mut found = false;
246 let mut max_length = 0;
247 for output in console.take_captured_out() {
248 match output {
249 CapturedOut::Print(msg) => {
250 if msg.contains("Type HELP") {
251 found = true;
252 max_length = std::cmp::max(max_length, msg.len());
253 }
254 }
255 _ => panic!("Unexpected console operation: {:?}", output),
256 }
257 }
258 (!found, max_length)
259 }
260
261 #[test]
262 fn test_print_welcome_wide_console() {
263 assert!(!check_is_narrow_welcome(50).0, "Long welcome not found");
264 }
265
266 #[test]
267 fn test_print_welcome_narrow_console() {
268 assert!(check_is_narrow_welcome(10).0, "Long welcome found");
269 }
270
271 #[test]
272 fn test_print_welcome_and_is_narrow_agree() {
273 let (narrow, max_length) = check_is_narrow_welcome(1000);
274 assert!(!narrow, "Long message not found");
275
276 for i in 0..max_length {
277 assert!(check_is_narrow_welcome(u16::try_from(i).unwrap()).0, "Long message found");
278 }
279 }
280
281 #[test]
282 fn test_autoexec_ok() {
283 let autoexec = "PRINT \"hello\": global_var = 3: CD \"MEMORY:/\"";
287 let mut tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
288 let (console, storage) = (tester.get_console(), tester.get_storage());
289 block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
290 tester
291 .run("")
292 .expect_var("global_var", 3)
293 .expect_prints(["hello"])
294 .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
295 .check();
296 }
297
298 #[test]
299 fn test_autoexec_compilation_error_is_ignored() {
300 let autoexec = "a = 1\nb = undef: c = 2";
301 let mut tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
302 let (console, storage) = (tester.get_console(), tester.get_storage());
303 block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
304 tester
305 .run("after = 5")
306 .expect_var("after", 5)
307 .expect_prints(["AUTOEXEC.BAS failed: 2:5: Undefined symbol undef"])
308 .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
309 .check();
310 }
311
312 #[test]
313 fn test_autoexec_execution_error_is_ignored() {
314 let autoexec = "a = 1\nb = 3 >> -1: c = 2";
315 let mut tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
316 let (console, storage) = (tester.get_console(), tester.get_storage());
317 block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
318 tester
319 .run("after = 5")
320 .expect_var("a", 1)
321 .expect_var("after", 5)
322 .expect_prints(["AUTOEXEC.BAS failed: 2:7: Number of bits to >> (-1) must be positive"])
323 .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
324 .check();
325 }
326
327 #[test]
328 fn test_autoexec_name_is_case_sensitive() {
329 let mut tester = Tester::default()
330 .write_file("AUTOEXEC.BAS", "a = 1")
331 .write_file("autoexec.bas", "a = 2");
332 let (console, storage) = (tester.get_console(), tester.get_storage());
333 block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
334 tester
335 .run("")
336 .expect_var("a", 1)
337 .expect_file("MEMORY:/AUTOEXEC.BAS", "a = 1")
338 .expect_file("MEMORY:/autoexec.bas", "a = 2")
339 .check();
340 }
341
342 #[test]
343 fn test_autoexec_missing() {
344 let mut tester = Tester::default();
345 let (console, storage) = (tester.get_console(), tester.get_storage());
346 block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
347 tester.run("").check();
348 }
349
350 struct MockDriveFactory {
352 exp_username: &'static str,
353 exp_file: &'static str,
354 }
355
356 impl MockDriveFactory {
357 const SCRIPT: &'static str = r#"PRINT "Success""#;
359 }
360
361 impl DriveFactory for MockDriveFactory {
362 fn create(&self, target: &str) -> io::Result<Box<dyn Drive>> {
363 let mut drive = InMemoryDrive::default();
364 block_on(drive.put(self.exp_file, Self::SCRIPT.as_bytes())).unwrap();
365 assert_eq!(self.exp_username, target);
366 Ok(Box::from(drive))
367 }
368 }
369
370 #[test]
371 fn test_run_from_cloud_no_repl() {
372 let mut tester = Tester::default();
373 let (console, storage, program) =
374 (tester.get_console(), tester.get_storage(), tester.get_program());
375
376 storage.borrow_mut().register_scheme(
377 "cloud",
378 Box::from(MockDriveFactory { exp_username: "foo", exp_file: "bar.bas" }),
379 );
380
381 block_on(run_from_cloud(
382 tester.get_machine(),
383 console,
384 storage,
385 program,
386 "foo/bar.bas",
387 false,
388 ))
389 .unwrap();
390 tester
391 .run("")
392 .expect_prints([
393 "Mounting cloud://foo as AUTORUN...",
394 "Loading AUTORUN:/bar.bas...",
395 "Starting...",
396 "",
397 ])
398 .expect_clear()
399 .expect_prints(["Success", "", "**** Program exited due to EOF ****"])
400 .expect_program(Some("AUTORUN:/bar.bas"), MockDriveFactory::SCRIPT)
401 .check();
402 }
403
404 #[test]
405 fn test_run_from_cloud_repl() {
406 let mut tester = Tester::default();
407 let (console, storage, program) =
408 (tester.get_console(), tester.get_storage(), tester.get_program());
409
410 storage.borrow_mut().register_scheme(
411 "cloud",
412 Box::from(MockDriveFactory { exp_username: "abcd", exp_file: "the-path.bas" }),
413 );
414
415 block_on(run_from_cloud(
416 tester.get_machine(),
417 console,
418 storage,
419 program,
420 "abcd/the-path.bas",
421 true,
422 ))
423 .unwrap();
424 let mut checker = tester.run("");
425 let output = flatten_output(checker.take_captured_out());
426 checker.expect_program(Some("AUTORUN:/the-path.bas"), MockDriveFactory::SCRIPT).check();
427
428 assert!(output.contains("You are now being dropped into"));
429 }
430
431 #[test]
432 fn test_run_repl_loop_signal_before_exec() {
433 let mut tester = Tester::default();
434 let (console, program) = (tester.get_console(), tester.get_program());
435 let signals_tx = tester.get_machine().get_signals_tx();
436
437 {
438 let mut console = console.borrow_mut();
439 console.add_input_chars("PRINT");
440 block_on(signals_tx.send(Signal::Break)).unwrap();
441 console.add_input_chars(" 123");
442 console.add_input_keys(&[Key::NewLine, Key::Eof]);
443 }
444 block_on(run_repl_loop(tester.get_machine(), console, program)).unwrap();
445 tester.run("").expect_prints([" 123", "End of input by CTRL-D"]).check();
446 }
447}