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