1use endbasic_std::console::{self, Console, PixelsXY, 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;
29mod logo;
30
31fn print_wide_welcome(console: &mut dyn Console, extra_indent: &str) -> io::Result<()> {
34 console.print("")?;
35 console.print(&format!(" {}EndBASIC {}", extra_indent, env!("CARGO_PKG_VERSION")))?;
36 console.print(&format!(" {}Copyright 2020-2026 Julio Merino", extra_indent))?;
37 console.print("")?;
38 console.print(" Type HELP for interactive usage information.")
39}
40
41fn print_graphical_welcome(console: &mut dyn Console) -> io::Result<()> {
43 let glyph_size = console.glyph_size()?;
44
45 let previous_sync = console.set_sync(false)?;
46 let result = (|| {
47 print_wide_welcome(console, " ")?;
48
49 let x1 = i32::from(glyph_size.width) * 4;
50 let x2 = i32::from(glyph_size.width) * 8;
51 let y1 = i32::from(glyph_size.height) / 2;
52 let y2 = i32::from(glyph_size.height) * 7 / 2;
53 logo::draw_logo(
54 console,
55 PixelsXY::new(x1 as i16, y1 as i16),
56 Some(PixelsXY::new(x2 as i16, y2 as i16)),
57 )?;
58
59 Ok(())
60 })();
61 console.set_sync(previous_sync)?;
62 result
63}
64
65fn has_graphics(console: &dyn Console) -> bool {
67 console.size_pixels().is_ok() && console.glyph_size().is_ok()
68}
69
70pub fn print_welcome(console: &mut dyn Console) -> io::Result<()> {
72 if is_narrow(&*console) {
73 console.print(&format!("EndBASIC {}", env!("CARGO_PKG_VERSION")))?;
74 } else if has_graphics(&*console) {
75 print_graphical_welcome(&mut *console)?;
76 } else {
77 print_wide_welcome(console, "")?;
78 }
79 console.print("")?;
80
81 Ok(())
82}
83
84pub async fn try_load_autoexec(
89 machine: &mut Machine,
90 console: Rc<RefCell<dyn Console>>,
91 storage: Rc<RefCell<Storage>>,
92) -> io::Result<()> {
93 let code = match storage.borrow().get("AUTOEXEC.BAS").await {
94 Ok(code) => code,
95 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
96 Err(e) => {
97 return console
98 .borrow_mut()
99 .print(&format!("AUTOEXEC.BAS exists but cannot be read: {}", e));
100 }
101 };
102
103 match machine.compile(&mut code.as_slice()) {
104 Ok(()) => match machine.exec().await {
105 Ok(_) => Ok(()),
106 Err(e) => {
107 console.borrow_mut().print(&format!("AUTOEXEC.BAS failed: {}", e))?;
108 Ok(())
109 }
110 },
111 Err(e) => {
112 console.borrow_mut().print(&format!("AUTOEXEC.BAS failed: {}", e))?;
113 Ok(())
114 }
115 }
116}
117
118pub fn mount_cloud_share(
120 console: Rc<RefCell<dyn Console>>,
121 storage: Rc<RefCell<Storage>>,
122 username_path: &str,
123) -> io::Result<String> {
124 let (fs_uri, path) = match username_path.split_once('/') {
125 Some((username, path)) => (format!("cloud://{}", username), format!("AUTORUN:/{}", path)),
126 None => {
127 return Err(io::Error::new(
128 io::ErrorKind::InvalidInput,
129 format!(
130 "Invalid program to run '{}'; must be of the form 'username/path'",
131 username_path
132 ),
133 ));
134 }
135 };
136
137 console.borrow_mut().print(&format!("Mounting {} as AUTORUN...", fs_uri))?;
138 storage.borrow_mut().mount("AUTORUN", &fs_uri)?;
139 storage.borrow_mut().cd("AUTORUN:/")?;
140 Ok(path)
141}
142
143pub async fn run_from_storage_path(
145 machine: &mut Machine,
146 console: Rc<RefCell<dyn Console>>,
147 storage: Rc<RefCell<Storage>>,
148 program: Rc<RefCell<dyn Program>>,
149 path: &str,
150 will_run_repl: bool,
151) -> io::Result<i32> {
152 let path = storage.borrow().make_canonical_with_extension(path, "bas")?;
153
154 console.borrow_mut().print(&format!("Loading {}...", path))?;
155 let content = storage.borrow().get(&path).await?;
156 let content = match String::from_utf8(content) {
157 Ok(text) => text,
158 Err(e) => {
159 let mut console = console.borrow_mut();
160 console.print(&format!("Invalid program to run '{}': {}", path, e))?;
161 return Ok(1);
162 }
163 };
164 program.borrow_mut().load(Some(&path), &content);
165
166 console.borrow_mut().print("Starting...")?;
167 console.borrow_mut().print("")?;
168
169 if let Err(e) = machine.compile(&mut "RUN".as_bytes()) {
170 let mut console = console.borrow_mut();
171 console.print(&format!("**** ERROR: {} ****", e))?;
172 return Ok(1);
173 }
174
175 let result = machine.exec().await;
176
177 let mut console = console.borrow_mut();
178
179 console.print("")?;
180 let code = match result {
181 Ok(None) => {
182 console.print("**** Program exited due to EOF ****")?;
183 0
184 }
185 Ok(Some(code)) => {
186 console.print(&format!("**** Program exited with code {} ****", code))?;
187 code
188 }
189 Err(StdError::Break) => {
190 console.print("**** Program stopped due to BREAK ****")?;
191 130
192 }
193 Err(e) => {
194 console.print(&format!("**** ERROR: {} ****", e))?;
195 1
196 }
197 };
198
199 if will_run_repl {
200 console.print("")?;
201 refill_and_print(
202 &mut *console,
203 [
204 "You are now being dropped into the EndBASIC interpreter.",
205 "The program you asked to run is still loaded in memory and you can interact with \
206 it now. Use LIST to view the source code, EDIT to launch an editor on the source code, and RUN to \
207 execute the program again.",
208 "Type HELP for interactive usage information.",
209 ],
210 " ",
211 )?;
212 console.print("")?;
213 }
214
215 Ok(code)
216}
217
218pub async fn run_repl_loop(
223 machine: &mut Machine,
224 console: Rc<RefCell<dyn Console>>,
225 program: Rc<RefCell<dyn Program>>,
226) -> io::Result<i32> {
227 let mut stop_reason = None;
228 let mut history = vec![];
229 while stop_reason.is_none() {
230 let line = {
231 let mut console = console.borrow_mut();
232 if console.is_interactive() {
233 console.print("Ready")?;
234 }
235 console::read_line(&mut *console, "", "", Some(&mut history)).await
236 };
237
238 machine.drain_signals();
241
242 match line {
243 Ok(line) => match machine.compile(&mut line.as_bytes()) {
244 Ok(()) => match machine.exec().await {
245 Ok(None) => stop_reason = None,
246 Ok(Some(code)) => {
247 let should_continue = {
248 let program = program.borrow();
249 let mut console = console.borrow_mut();
250 continue_if_modified(&*program, &mut *console).await?
251 };
252 if should_continue {
253 stop_reason = Some(code);
254 } else {
255 let mut console = console.borrow_mut();
256 console.print("Exit aborted; resuming REPL loop.")?;
257 }
258 }
259 Err(StdError::Break) => {
260 let mut console = console.borrow_mut();
261 console.print(BREAK_MSG)?;
262 }
263 Err(e) => {
264 let mut console = console.borrow_mut();
265 console.print(format!("ERROR: {}", e).as_str())?;
266 }
267 },
268 Err(e) => {
269 let mut console = console.borrow_mut();
270 console.print(format!("ERROR: {}", e).as_str())?;
271 }
272 },
273 Err(e) => {
274 if e.kind() == io::ErrorKind::Interrupted {
275 let mut console = console.borrow_mut();
276 console.print(BREAK_MSG)?;
277 } else if e.kind() == io::ErrorKind::UnexpectedEof {
281 let mut console = console.borrow_mut();
282 console.print("End of input by CTRL-D")?;
283 stop_reason = Some(0);
284 } else {
285 stop_reason = Some(1);
286 }
287 }
288 }
289 }
290 Ok(stop_reason.unwrap())
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use endbasic_sdl::testutils::SdlTest;
297 use endbasic_std::Signal;
298 use endbasic_std::console::{CharsXY, Key};
299 use endbasic_std::gfx::lcd::fonts::{FONT_5X8, FONT_16X16};
300 use endbasic_std::storage::{Drive, DriveFactory, InMemoryDrive};
301 use endbasic_std::testutils::*;
302 use futures_lite::future::block_on;
303
304 fn check_is_narrow_welcome(console_width: u16) -> (bool, usize) {
308 let console = Rc::from(RefCell::from(MockConsole::default()));
309 console.borrow_mut().set_size_chars(CharsXY::new(console_width, 1));
310 print_welcome(&mut *console.borrow_mut()).unwrap();
311
312 let mut console = console.borrow_mut();
313 let mut found = false;
314 let mut max_length = 0;
315 for output in console.take_captured_out() {
316 match output {
317 CapturedOut::Print(msg) => {
318 if msg.contains("Type HELP") {
319 found = true;
320 max_length = std::cmp::max(max_length, msg.len());
321 }
322 }
323 _ => panic!("Unexpected console operation: {:?}", output),
324 }
325 }
326 (!found, max_length)
327 }
328
329 #[test]
330 fn test_print_welcome_wide_console() {
331 assert!(!check_is_narrow_welcome(50).0, "Long welcome not found");
332 }
333
334 #[test]
335 fn test_print_welcome_narrow_console() {
336 assert!(check_is_narrow_welcome(10).0, "Long welcome found");
337 }
338
339 #[test]
340 fn test_print_welcome_and_is_narrow_agree() {
341 let (narrow, max_length) = check_is_narrow_welcome(1000);
342 assert!(!narrow, "Long message not found");
343
344 for i in 0..max_length {
345 assert!(check_is_narrow_welcome(u16::try_from(i).unwrap()).0, "Long message found");
346 }
347 }
348
349 #[test]
350 fn test_print_welcome_draw_logo_default() {
351 let mut test = SdlTest::default();
352 print_welcome(test.console()).unwrap();
353 test.verify("repl/src", "welcome-banner-default");
354 }
355
356 #[test]
357 fn test_print_welcome_draw_logo_tiny() {
358 let mut test = SdlTest::new(800, 600, &FONT_5X8);
359 print_welcome(test.console()).unwrap();
360 test.verify("repl/src", "welcome-banner-tiny");
361 }
362
363 #[test]
364 fn test_print_welcome_draw_logo_big() {
365 let mut test = SdlTest::new(800, 600, &FONT_16X16);
366 print_welcome(test.console()).unwrap();
367 test.verify("repl/src", "welcome-banner-big");
368 }
369
370 #[test]
371 fn test_autoexec_ok() {
372 let autoexec = "PRINT \"hello\": global_var = 3: CD \"MEMORY:/\"";
376 let tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
377 let (console, storage) = (tester.get_console(), tester.get_storage());
378 let mut continuation = tester.continue_from_here();
379 block_on(try_load_autoexec(continuation.get_machine(), console, storage)).unwrap();
380 continuation
381 .run("")
382 .expect_prints(["hello"])
383 .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
384 .check();
385 }
386
387 #[test]
388 fn test_autoexec_compilation_error_is_ignored() {
389 let autoexec = "a = 1\nb = undef: c = 2";
390 let tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
391 let (console, storage) = (tester.get_console(), tester.get_storage());
392 let mut continuation = tester.continue_from_here();
393 block_on(try_load_autoexec(continuation.get_machine(), console, storage)).unwrap();
394 continuation
395 .run("after = 5")
396 .expect_var("after", 5)
397 .expect_prints(["AUTOEXEC.BAS failed: 2:5: Undefined symbol undef"])
398 .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
399 .check();
400 }
401
402 #[test]
403 fn test_autoexec_execution_error_is_ignored() {
404 let autoexec = "a = 1\nb = 3 >> -1: c = 2";
405 let tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
406 let (console, storage) = (tester.get_console(), tester.get_storage());
407 let mut continuation = tester.continue_from_here();
408 block_on(try_load_autoexec(continuation.get_machine(), console, storage)).unwrap();
409 continuation
410 .run("after = 5")
411 .expect_prints(["AUTOEXEC.BAS failed: 2:7: Number of bits to >> (-1) must be positive"])
412 .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
413 .check();
414 }
415
416 #[test]
417 fn test_autoexec_name_is_case_sensitive() {
418 let tester = Tester::default()
419 .write_file("AUTOEXEC.BAS", "a = 1")
420 .write_file("autoexec.bas", "a = 2");
421 let (console, storage) = (tester.get_console(), tester.get_storage());
422 let mut continuation = tester.continue_from_here();
423 block_on(try_load_autoexec(continuation.get_machine(), console, storage)).unwrap();
424 continuation
425 .run("")
426 .expect_file("MEMORY:/AUTOEXEC.BAS", "a = 1")
427 .expect_file("MEMORY:/autoexec.bas", "a = 2")
428 .check();
429 }
430
431 #[test]
432 fn test_autoexec_missing() {
433 let tester = Tester::default();
434 let (console, storage) = (tester.get_console(), tester.get_storage());
435 let mut continuation = tester.continue_from_here();
436 block_on(try_load_autoexec(continuation.get_machine(), console, storage)).unwrap();
437 continuation.run("").check();
438 }
439
440 struct MockDriveFactory {
442 exp_username: &'static str,
443 exp_file: &'static str,
444 }
445
446 impl MockDriveFactory {
447 const SCRIPT: &'static str = r#"PRINT "Success""#;
449 }
450
451 impl DriveFactory for MockDriveFactory {
452 fn create(&self, target: &str) -> io::Result<Box<dyn Drive>> {
453 let mut drive = InMemoryDrive::default();
454 block_on(drive.put(self.exp_file, Self::SCRIPT.as_bytes())).unwrap();
455 assert_eq!(self.exp_username, target);
456 Ok(Box::from(drive))
457 }
458 }
459
460 #[test]
461 fn test_mount_cloud_share_invalid_path() {
462 let tester = Tester::default();
463 let (console, storage) = (tester.get_console(), tester.get_storage());
464
465 let e = mount_cloud_share(console, storage, "foo").unwrap_err();
466 assert_eq!(io::ErrorKind::InvalidInput, e.kind());
467 assert_eq!(
468 "Invalid program to run 'foo'; must be of the form 'username/path'",
469 format!("{}", e)
470 );
471 }
472
473 #[test]
474 fn test_mount_cloud_share_ok() {
475 let tester = Tester::default();
476 let (console, storage) = (tester.get_console(), tester.get_storage());
477 let continuation = tester.continue_from_here();
478
479 storage.borrow_mut().register_scheme(
480 "cloud",
481 Box::from(MockDriveFactory { exp_username: "foo", exp_file: "bar.bas" }),
482 );
483
484 let path = mount_cloud_share(console, storage, "foo/bar.bas").unwrap();
485 assert_eq!("AUTORUN:/bar.bas", path);
486 assert_eq!(Some(&"cloud://foo"), tester.get_storage().borrow().mounted().get("AUTORUN"));
487 assert_eq!("AUTORUN:/", tester.get_storage().borrow().cwd());
488 continuation.run("").expect_prints(["Mounting cloud://foo as AUTORUN..."]).check();
489 }
490
491 #[test]
492 fn test_run_from_storage_path_no_repl() {
493 let tester = Tester::default();
494 let (console, storage, program) =
495 (tester.get_console(), tester.get_storage(), tester.get_program());
496 let mut continuation = tester.continue_from_here();
497
498 storage.borrow_mut().mount("SOME", "memory://").unwrap();
499 block_on(storage.borrow_mut().put("SOME:bar.bas", MockDriveFactory::SCRIPT.as_bytes()))
500 .unwrap();
501
502 block_on(run_from_storage_path(
503 continuation.get_machine(),
504 console,
505 storage,
506 program,
507 "some:bar.bas",
508 false,
509 ))
510 .unwrap();
511 continuation
512 .run("")
513 .expect_prints(["Loading SOME:bar.bas...", "Starting...", ""])
514 .expect_clear()
515 .expect_prints(["Success", "", "**** Program exited due to EOF ****"])
516 .expect_file("SOME:/bar.bas", MockDriveFactory::SCRIPT)
517 .expect_program(Some("SOME:bar.bas"), MockDriveFactory::SCRIPT)
518 .check();
519 }
520
521 #[test]
522 fn test_run_from_storage_path_with_default_extension_and_repl() {
523 let tester = Tester::default().write_file("demo.bas", MockDriveFactory::SCRIPT);
524 let (console, storage, program) =
525 (tester.get_console(), tester.get_storage(), tester.get_program());
526 let mut continuation = tester.continue_from_here();
527
528 block_on(run_from_storage_path(
529 continuation.get_machine(),
530 console,
531 storage,
532 program,
533 "memory:demo",
534 true,
535 ))
536 .unwrap();
537 let mut checker = continuation.run("");
538 let output = flatten_output(checker.take_captured_out());
539 checker
540 .expect_file("MEMORY:/demo.bas", MockDriveFactory::SCRIPT)
541 .expect_program(Some("MEMORY:demo.bas"), MockDriveFactory::SCRIPT)
542 .check();
543
544 assert!(output.contains("Loading MEMORY:demo.bas..."));
545 assert!(output.contains("You are now being dropped into"));
546 }
547
548 #[test]
549 fn test_run_repl_loop_signal_before_exec() {
550 let mut tester = Tester::default();
551 let (console, program) = (tester.get_console(), tester.get_program());
552 let (signals_tx, signals_rx) = async_channel::unbounded();
553 let mut machine = endbasic_std::MachineBuilder::default()
554 .with_console(console.clone())
555 .with_signals_chan((signals_tx.clone(), signals_rx))
556 .build();
557
558 {
559 let mut console = console.borrow_mut();
560 console.add_input_chars("PRINT");
561 block_on(signals_tx.send(Signal::Break)).unwrap();
562 console.add_input_chars(" 123");
563 console.add_input_keys(&[Key::NewLine, Key::EofOrDelete]);
564 }
565 block_on(run_repl_loop(&mut machine, console, program)).unwrap();
566 tester.run("").expect_prints([" 123", "End of input by CTRL-D"]).check();
567 }
568
569 #[test]
570 fn test_run_repl_loop_eof_during_input_does_not_exit_repl() {
571 let mut tester = Tester::default();
572 let (console, program) = (tester.get_console(), tester.get_program());
573 let mut machine =
574 endbasic_std::MachineBuilder::default().with_console(console.clone()).build();
575
576 {
577 let mut console = console.borrow_mut();
578 console.add_input_chars("INPUT a\n");
579 console.add_input_keys(&[Key::EofOrDelete]);
580 console.add_input_chars("PRINT 3\n");
581 console.add_input_keys(&[Key::EofOrDelete]);
582 }
583 block_on(run_repl_loop(&mut machine, console, program)).unwrap();
584 tester.run("").expect_prints(["ERROR: 1:1: EOF", " 3", "End of input by CTRL-D"]).check();
585 }
586}