use endbasic_std::console::{self, Console, PixelsXY, is_narrow, refill_and_print};
use endbasic_std::program::{BREAK_MSG, Program, continue_if_modified};
use endbasic_std::storage::Storage;
use endbasic_std::{Error as StdError, Machine};
use std::cell::RefCell;
use std::io;
use std::rc::Rc;
pub mod demos;
pub mod editor;
mod logo;
fn print_wide_welcome(console: &mut dyn Console, extra_indent: &str) -> io::Result<()> {
console.print("")?;
console.print(&format!(" {}EndBASIC {}", extra_indent, env!("CARGO_PKG_VERSION")))?;
console.print(&format!(" {}Copyright 2020-2026 Julio Merino", extra_indent))?;
console.print("")?;
console.print(" Type HELP for interactive usage information.")
}
fn print_graphical_welcome(console: &mut dyn Console) -> io::Result<()> {
let glyph_size = console.glyph_size()?;
let previous_sync = console.set_sync(false)?;
let result = (|| {
print_wide_welcome(console, " ")?;
let x1 = i32::from(glyph_size.width) * 4;
let x2 = i32::from(glyph_size.width) * 8;
let y1 = i32::from(glyph_size.height) / 2;
let y2 = i32::from(glyph_size.height) * 7 / 2;
logo::draw_logo(
console,
PixelsXY::new(x1 as i16, y1 as i16),
Some(PixelsXY::new(x2 as i16, y2 as i16)),
)?;
Ok(())
})();
console.set_sync(previous_sync)?;
result
}
fn has_graphics(console: &dyn Console) -> bool {
console.size_pixels().is_ok() && console.glyph_size().is_ok()
}
pub fn print_welcome(console: &mut dyn Console) -> io::Result<()> {
if is_narrow(&*console) {
console.print(&format!("EndBASIC {}", env!("CARGO_PKG_VERSION")))?;
} else if has_graphics(&*console) {
print_graphical_welcome(&mut *console)?;
} else {
print_wide_welcome(console, "")?;
}
console.print("")?;
Ok(())
}
pub async fn try_load_autoexec(
machine: &mut Machine,
console: Rc<RefCell<dyn Console>>,
storage: Rc<RefCell<Storage>>,
) -> io::Result<()> {
let code = match storage.borrow().get("AUTOEXEC.BAS").await {
Ok(code) => code,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => {
return console
.borrow_mut()
.print(&format!("AUTOEXEC.BAS exists but cannot be read: {}", e));
}
};
match machine.compile(&mut code.as_slice()) {
Ok(()) => match machine.exec().await {
Ok(_) => Ok(()),
Err(e) => {
console.borrow_mut().print(&format!("AUTOEXEC.BAS failed: {}", e))?;
Ok(())
}
},
Err(e) => {
console.borrow_mut().print(&format!("AUTOEXEC.BAS failed: {}", e))?;
Ok(())
}
}
}
pub fn mount_cloud_share(
console: Rc<RefCell<dyn Console>>,
storage: Rc<RefCell<Storage>>,
username_path: &str,
) -> io::Result<String> {
let (fs_uri, path) = match username_path.split_once('/') {
Some((username, path)) => (format!("cloud://{}", username), format!("AUTORUN:/{}", path)),
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Invalid program to run '{}'; must be of the form 'username/path'",
username_path
),
));
}
};
console.borrow_mut().print(&format!("Mounting {} as AUTORUN...", fs_uri))?;
storage.borrow_mut().mount("AUTORUN", &fs_uri)?;
storage.borrow_mut().cd("AUTORUN:/")?;
Ok(path)
}
pub async fn run_from_storage_path(
machine: &mut Machine,
console: Rc<RefCell<dyn Console>>,
storage: Rc<RefCell<Storage>>,
program: Rc<RefCell<dyn Program>>,
path: &str,
will_run_repl: bool,
) -> io::Result<i32> {
let path = storage.borrow().make_canonical_with_extension(path, "bas")?;
console.borrow_mut().print(&format!("Loading {}...", path))?;
let content = storage.borrow().get(&path).await?;
let content = match String::from_utf8(content) {
Ok(text) => text,
Err(e) => {
let mut console = console.borrow_mut();
console.print(&format!("Invalid program to run '{}': {}", path, e))?;
return Ok(1);
}
};
program.borrow_mut().load(Some(&path), &content);
console.borrow_mut().print("Starting...")?;
console.borrow_mut().print("")?;
if let Err(e) = machine.compile(&mut "RUN".as_bytes()) {
let mut console = console.borrow_mut();
console.print(&format!("**** ERROR: {} ****", e))?;
return Ok(1);
}
let result = machine.exec().await;
let mut console = console.borrow_mut();
console.print("")?;
let code = match result {
Ok(None) => {
console.print("**** Program exited due to EOF ****")?;
0
}
Ok(Some(code)) => {
console.print(&format!("**** Program exited with code {} ****", code))?;
code
}
Err(StdError::Break) => {
console.print("**** Program stopped due to BREAK ****")?;
130
}
Err(e) => {
console.print(&format!("**** ERROR: {} ****", e))?;
1
}
};
if will_run_repl {
console.print("")?;
refill_and_print(
&mut *console,
[
"You are now being dropped into the EndBASIC interpreter.",
"The program you asked to run is still loaded in memory and you can interact with \
it now. Use LIST to view the source code, EDIT to launch an editor on the source code, and RUN to \
execute the program again.",
"Type HELP for interactive usage information.",
],
" ",
)?;
console.print("")?;
}
Ok(code)
}
pub async fn run_repl_loop(
machine: &mut Machine,
console: Rc<RefCell<dyn Console>>,
program: Rc<RefCell<dyn Program>>,
) -> io::Result<i32> {
let mut stop_reason = None;
let mut history = vec![];
while stop_reason.is_none() {
let line = {
let mut console = console.borrow_mut();
if console.is_interactive() {
console.print("Ready")?;
}
console::read_line(&mut *console, "", "", Some(&mut history)).await
};
machine.drain_signals();
match line {
Ok(line) => match machine.compile(&mut line.as_bytes()) {
Ok(()) => match machine.exec().await {
Ok(None) => stop_reason = None,
Ok(Some(code)) => {
let should_continue = {
let program = program.borrow();
let mut console = console.borrow_mut();
continue_if_modified(&*program, &mut *console).await?
};
if should_continue {
stop_reason = Some(code);
} else {
let mut console = console.borrow_mut();
console.print("Exit aborted; resuming REPL loop.")?;
}
}
Err(StdError::Break) => {
let mut console = console.borrow_mut();
console.print(BREAK_MSG)?;
}
Err(e) => {
let mut console = console.borrow_mut();
console.print(format!("ERROR: {}", e).as_str())?;
}
},
Err(e) => {
let mut console = console.borrow_mut();
console.print(format!("ERROR: {}", e).as_str())?;
}
},
Err(e) => {
if e.kind() == io::ErrorKind::Interrupted {
let mut console = console.borrow_mut();
console.print(BREAK_MSG)?;
} else if e.kind() == io::ErrorKind::UnexpectedEof {
let mut console = console.borrow_mut();
console.print("End of input by CTRL-D")?;
stop_reason = Some(0);
} else {
stop_reason = Some(1);
}
}
}
}
Ok(stop_reason.unwrap())
}
#[cfg(test)]
mod tests {
use super::*;
use endbasic_sdl::testutils::SdlTest;
use endbasic_std::Signal;
use endbasic_std::console::{CharsXY, Key};
use endbasic_std::gfx::lcd::fonts::{FONT_5X8, FONT_16X16};
use endbasic_std::storage::{Drive, DriveFactory, InMemoryDrive};
use endbasic_std::testutils::*;
use futures_lite::future::block_on;
fn check_is_narrow_welcome(console_width: u16) -> (bool, usize) {
let console = Rc::from(RefCell::from(MockConsole::default()));
console.borrow_mut().set_size_chars(CharsXY::new(console_width, 1));
print_welcome(&mut *console.borrow_mut()).unwrap();
let mut console = console.borrow_mut();
let mut found = false;
let mut max_length = 0;
for output in console.take_captured_out() {
match output {
CapturedOut::Print(msg) => {
if msg.contains("Type HELP") {
found = true;
max_length = std::cmp::max(max_length, msg.len());
}
}
_ => panic!("Unexpected console operation: {:?}", output),
}
}
(!found, max_length)
}
#[test]
fn test_print_welcome_wide_console() {
assert!(!check_is_narrow_welcome(50).0, "Long welcome not found");
}
#[test]
fn test_print_welcome_narrow_console() {
assert!(check_is_narrow_welcome(10).0, "Long welcome found");
}
#[test]
fn test_print_welcome_and_is_narrow_agree() {
let (narrow, max_length) = check_is_narrow_welcome(1000);
assert!(!narrow, "Long message not found");
for i in 0..max_length {
assert!(check_is_narrow_welcome(u16::try_from(i).unwrap()).0, "Long message found");
}
}
#[test]
fn test_print_welcome_draw_logo_default() {
let mut test = SdlTest::default();
print_welcome(test.console()).unwrap();
test.verify("repl/src", "welcome-banner-default");
}
#[test]
fn test_print_welcome_draw_logo_tiny() {
let mut test = SdlTest::new(800, 600, &FONT_5X8);
print_welcome(test.console()).unwrap();
test.verify("repl/src", "welcome-banner-tiny");
}
#[test]
fn test_print_welcome_draw_logo_big() {
let mut test = SdlTest::new(800, 600, &FONT_16X16);
print_welcome(test.console()).unwrap();
test.verify("repl/src", "welcome-banner-big");
}
#[test]
fn test_autoexec_ok() {
let autoexec = "PRINT \"hello\": global_var = 3: CD \"MEMORY:/\"";
let tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
let (console, storage) = (tester.get_console(), tester.get_storage());
let mut continuation = tester.continue_from_here();
block_on(try_load_autoexec(continuation.get_machine(), console, storage)).unwrap();
continuation
.run("")
.expect_prints(["hello"])
.expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
.check();
}
#[test]
fn test_autoexec_compilation_error_is_ignored() {
let autoexec = "a = 1\nb = undef: c = 2";
let tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
let (console, storage) = (tester.get_console(), tester.get_storage());
let mut continuation = tester.continue_from_here();
block_on(try_load_autoexec(continuation.get_machine(), console, storage)).unwrap();
continuation
.run("after = 5")
.expect_var("after", 5)
.expect_prints(["AUTOEXEC.BAS failed: 2:5: Undefined symbol undef"])
.expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
.check();
}
#[test]
fn test_autoexec_execution_error_is_ignored() {
let autoexec = "a = 1\nb = 3 >> -1: c = 2";
let tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
let (console, storage) = (tester.get_console(), tester.get_storage());
let mut continuation = tester.continue_from_here();
block_on(try_load_autoexec(continuation.get_machine(), console, storage)).unwrap();
continuation
.run("after = 5")
.expect_prints(["AUTOEXEC.BAS failed: 2:7: Number of bits to >> (-1) must be positive"])
.expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
.check();
}
#[test]
fn test_autoexec_name_is_case_sensitive() {
let tester = Tester::default()
.write_file("AUTOEXEC.BAS", "a = 1")
.write_file("autoexec.bas", "a = 2");
let (console, storage) = (tester.get_console(), tester.get_storage());
let mut continuation = tester.continue_from_here();
block_on(try_load_autoexec(continuation.get_machine(), console, storage)).unwrap();
continuation
.run("")
.expect_file("MEMORY:/AUTOEXEC.BAS", "a = 1")
.expect_file("MEMORY:/autoexec.bas", "a = 2")
.check();
}
#[test]
fn test_autoexec_missing() {
let tester = Tester::default();
let (console, storage) = (tester.get_console(), tester.get_storage());
let mut continuation = tester.continue_from_here();
block_on(try_load_autoexec(continuation.get_machine(), console, storage)).unwrap();
continuation.run("").check();
}
struct MockDriveFactory {
exp_username: &'static str,
exp_file: &'static str,
}
impl MockDriveFactory {
const SCRIPT: &'static str = r#"PRINT "Success""#;
}
impl DriveFactory for MockDriveFactory {
fn create(&self, target: &str) -> io::Result<Box<dyn Drive>> {
let mut drive = InMemoryDrive::default();
block_on(drive.put(self.exp_file, Self::SCRIPT.as_bytes())).unwrap();
assert_eq!(self.exp_username, target);
Ok(Box::from(drive))
}
}
#[test]
fn test_mount_cloud_share_invalid_path() {
let tester = Tester::default();
let (console, storage) = (tester.get_console(), tester.get_storage());
let e = mount_cloud_share(console, storage, "foo").unwrap_err();
assert_eq!(io::ErrorKind::InvalidInput, e.kind());
assert_eq!(
"Invalid program to run 'foo'; must be of the form 'username/path'",
format!("{}", e)
);
}
#[test]
fn test_mount_cloud_share_ok() {
let tester = Tester::default();
let (console, storage) = (tester.get_console(), tester.get_storage());
let continuation = tester.continue_from_here();
storage.borrow_mut().register_scheme(
"cloud",
Box::from(MockDriveFactory { exp_username: "foo", exp_file: "bar.bas" }),
);
let path = mount_cloud_share(console, storage, "foo/bar.bas").unwrap();
assert_eq!("AUTORUN:/bar.bas", path);
assert_eq!(Some(&"cloud://foo"), tester.get_storage().borrow().mounted().get("AUTORUN"));
assert_eq!("AUTORUN:/", tester.get_storage().borrow().cwd());
continuation.run("").expect_prints(["Mounting cloud://foo as AUTORUN..."]).check();
}
#[test]
fn test_run_from_storage_path_no_repl() {
let tester = Tester::default();
let (console, storage, program) =
(tester.get_console(), tester.get_storage(), tester.get_program());
let mut continuation = tester.continue_from_here();
storage.borrow_mut().mount("SOME", "memory://").unwrap();
block_on(storage.borrow_mut().put("SOME:bar.bas", MockDriveFactory::SCRIPT.as_bytes()))
.unwrap();
block_on(run_from_storage_path(
continuation.get_machine(),
console,
storage,
program,
"some:bar.bas",
false,
))
.unwrap();
continuation
.run("")
.expect_prints(["Loading SOME:bar.bas...", "Starting...", ""])
.expect_clear()
.expect_prints(["Success", "", "**** Program exited due to EOF ****"])
.expect_file("SOME:/bar.bas", MockDriveFactory::SCRIPT)
.expect_program(Some("SOME:bar.bas"), MockDriveFactory::SCRIPT)
.check();
}
#[test]
fn test_run_from_storage_path_with_default_extension_and_repl() {
let tester = Tester::default().write_file("demo.bas", MockDriveFactory::SCRIPT);
let (console, storage, program) =
(tester.get_console(), tester.get_storage(), tester.get_program());
let mut continuation = tester.continue_from_here();
block_on(run_from_storage_path(
continuation.get_machine(),
console,
storage,
program,
"memory:demo",
true,
))
.unwrap();
let mut checker = continuation.run("");
let output = flatten_output(checker.take_captured_out());
checker
.expect_file("MEMORY:/demo.bas", MockDriveFactory::SCRIPT)
.expect_program(Some("MEMORY:demo.bas"), MockDriveFactory::SCRIPT)
.check();
assert!(output.contains("Loading MEMORY:demo.bas..."));
assert!(output.contains("You are now being dropped into"));
}
#[test]
fn test_run_repl_loop_signal_before_exec() {
let mut tester = Tester::default();
let (console, program) = (tester.get_console(), tester.get_program());
let (signals_tx, signals_rx) = async_channel::unbounded();
let mut machine = endbasic_std::MachineBuilder::default()
.with_console(console.clone())
.with_signals_chan((signals_tx.clone(), signals_rx))
.build();
{
let mut console = console.borrow_mut();
console.add_input_chars("PRINT");
block_on(signals_tx.send(Signal::Break)).unwrap();
console.add_input_chars(" 123");
console.add_input_keys(&[Key::NewLine, Key::EofOrDelete]);
}
block_on(run_repl_loop(&mut machine, console, program)).unwrap();
tester.run("").expect_prints([" 123", "End of input by CTRL-D"]).check();
}
#[test]
fn test_run_repl_loop_eof_during_input_does_not_exit_repl() {
let mut tester = Tester::default();
let (console, program) = (tester.get_console(), tester.get_program());
let mut machine =
endbasic_std::MachineBuilder::default().with_console(console.clone()).build();
{
let mut console = console.borrow_mut();
console.add_input_chars("INPUT a\n");
console.add_input_keys(&[Key::EofOrDelete]);
console.add_input_chars("PRINT 3\n");
console.add_input_keys(&[Key::EofOrDelete]);
}
block_on(run_repl_loop(&mut machine, console, program)).unwrap();
tester.run("").expect_prints(["ERROR: 1:1: EOF", " 3", "End of input by CTRL-D"]).check();
}
}