use anyhow::{Result, anyhow};
use async_channel::Sender;
use endbasic_core::exec::Signal;
use endbasic_std::console::{Console, ConsoleSpec};
use endbasic_std::gpio;
use endbasic_std::storage::Storage;
use getoptsargs::prelude::*;
use std::cell::RefCell;
use std::fs::File;
use std::io;
use std::path::Path;
use std::rc::Rc;
#[derive(Debug, thiserror::Error)]
#[error("{message}")]
struct UsageError {
message: String,
}
impl UsageError {
fn new<T: Into<String>>(message: T) -> Self {
Self { message: message.into() }
}
}
fn app_extra_help(o: &mut dyn io::Write) -> io::Result<()> {
writeln!(o, "CONSOLE-SPEC can be one of the following:")?;
if cfg!(feature = "sdl") {
writeln!(o, " sdl[:SPEC] enables the graphical console and configures it")?;
writeln!(o, " with the settings in SPEC, which is of the form:")?;
writeln!(
o,
" resolution=RESOLUTION,fg_color=COLOR,bg_color=COLOR,"
)?;
writeln!(o, " font_path=TTF,font_size=SIZE")?;
writeln!(o, " individual components of the SPEC can be omitted")?;
writeln!(o, " RESOLUTION can be one of 'fs' (for full screen),")?;
writeln!(o, " 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs'")?;
}
if cfg!(feature = "rpi") {
writeln!(o, " st7735s[:SPEC] enables the ST7735S LCD console and configures it")?;
writeln!(o, " with the settings in SPEC, which is of the form:")?;
writeln!(o, " fg_color=COLOR,bg_color=COLOR,font=NAME")?;
}
writeln!(o, " text enables the text-based console")?;
writeln!(o)?;
writeln!(o, "GPIO-PINS-SPEC can be one of the following:")?;
writeln!(o, " mock mock backend for testing")?;
writeln!(o, " noop dummy backend that always returns errors")?;
if cfg!(feature = "rpi") {
writeln!(o, " rppal uses the Raspberry Pi GPIO hardware")?;
}
Ok(())
}
#[cfg(feature = "rpi")]
fn setup_gpio_pins_rppal() -> Result<Rc<RefCell<dyn gpio::Pins>>> {
Ok(Rc::new(RefCell::new(endbasic_rpi::RppalPins::default())))
}
#[cfg(not(feature = "rpi"))]
fn setup_gpio_pins_rppal() -> Result<Rc<RefCell<dyn gpio::Pins>>> {
Err(UsageError::new("--gpio-pins=rppal requires the rpi feature to be compiled in").into())
}
fn setup_gpio_pins(spec: Option<&str>) -> Result<Rc<RefCell<dyn gpio::Pins>>> {
let spec = if cfg!(feature = "rpi") { spec.unwrap_or("rppal") } else { spec.unwrap_or("noop") };
match spec {
"mock" => Ok(Rc::new(RefCell::new(gpio::MockPins::default()))),
"noop" => Ok(Rc::new(RefCell::new(gpio::NoopPins::default()))),
"rppal" => setup_gpio_pins_rppal(),
other => Err(UsageError::new(format!("Unknown --gpio-pins backend: {}", other)).into()),
}
}
fn new_machine_builder(
console_spec: Option<&str>,
gpio_pins_spec: Option<&str>,
) -> Result<endbasic_std::MachineBuilder> {
let signals_chan = async_channel::unbounded();
let mut builder = endbasic_std::MachineBuilder::default();
builder = builder.with_console(setup_console(console_spec, signals_chan.0.clone())?);
builder = builder.with_signals_chan(signals_chan);
builder = builder.with_gpio_pins(setup_gpio_pins(gpio_pins_spec)?);
Ok(builder)
}
fn make_interactive(
builder: endbasic_std::MachineBuilder,
) -> endbasic_std::InteractiveMachineBuilder {
builder
.make_interactive()
.with_program(Rc::from(RefCell::from(endbasic_repl::editor::Editor::default())))
}
fn finish_interactive_build(
mut builder: endbasic_std::InteractiveMachineBuilder,
service_url: &str,
) -> Result<endbasic_core::exec::Machine> {
let console = builder.get_console();
let storage = builder.get_storage();
let mut machine = builder.build()?;
let service = Rc::from(RefCell::from(endbasic_client::CloudService::new(service_url)?));
endbasic_client::add_all(&mut machine, service, console, storage, "https://repl.endbasic.dev/");
Ok(machine)
}
fn get_local_drive_spec(flag: Option<String>) -> Result<String> {
let dir = flag.or_else(|| {
dirs::document_dir().map(|d| format!("file://{}", d.join("endbasic").display())).or_else(
|| {
dirs::home_dir()
.map(|h| format!("file://{}", h.join("Documents/endbasic").display()))
},
)
});
match dir {
Some(dir) => Ok(dir),
None => Err(anyhow!("Cannot compute default path to the Documents folder")),
}
}
fn setup_console(
console_spec: Option<&str>,
signals_tx: Sender<Signal>,
) -> io::Result<Rc<RefCell<dyn Console>>> {
#[cfg(feature = "crossterm")]
fn setup_text_console(signals_tx: Sender<Signal>) -> io::Result<Rc<RefCell<dyn Console>>> {
Ok(Rc::from(RefCell::from(endbasic_terminal::TerminalConsole::from_stdio(signals_tx)?)))
}
#[cfg(not(feature = "crossterm"))]
fn setup_text_console(_signals_tx: Sender<Signal>) -> io::Result<Rc<RefCell<dyn Console>>> {
Ok(Rc::from(RefCell::from(endbasic_std::console::TrivialConsole::default())))
}
#[cfg(feature = "sdl")]
pub fn setup_sdl_console(
signals_tx: Sender<Signal>,
spec: &mut ConsoleSpec,
) -> io::Result<Rc<RefCell<dyn Console>>> {
endbasic_sdl::setup(spec, signals_tx)
}
#[cfg(not(feature = "sdl"))]
pub fn setup_sdl_console(
_signals_tx: Sender<Signal>,
_spec: &mut ConsoleSpec,
) -> io::Result<Rc<RefCell<dyn Console>>> {
Err(io::Error::new(io::ErrorKind::InvalidInput, "SDL support not compiled in"))
}
#[cfg(feature = "rpi")]
fn setup_st7735s_console(
signals_tx: Sender<Signal>,
spec: &mut ConsoleSpec,
) -> io::Result<Rc<RefCell<dyn Console>>> {
let console = endbasic_st7735s::new_console(
endbasic_rpi::RppalPins::default(),
endbasic_rpi::spi_bus_open,
endbasic_terminal::TerminalConsole::from_stdio(signals_tx)?,
spec,
&endbasic_std::gfx::lcd::fonts::all_fonts(),
)?;
Ok(Rc::from(RefCell::from(console)))
}
#[cfg(not(feature = "rpi"))]
pub fn setup_st7735s_console(
_signals_tx: Sender<Signal>,
_spec: &mut ConsoleSpec,
) -> io::Result<Rc<RefCell<dyn Console>>> {
Err(io::Error::new(io::ErrorKind::InvalidInput, "ST7735S support not compiled in"))
}
let mut console_spec = ConsoleSpec::init(console_spec.unwrap_or("text"));
let console: Rc<RefCell<dyn Console>> = match console_spec.driver {
"sdl" => setup_sdl_console(signals_tx, &mut console_spec)?,
"st7735s" => setup_st7735s_console(signals_tx, &mut console_spec)?,
"text" => setup_text_console(signals_tx)?,
driver => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Unknown console driver {}", driver),
));
}
};
console_spec.finish().map_err(|e| {
io::Error::new(io::ErrorKind::InvalidInput, format!("Invalid --console flag: {}", e))
})?;
Ok(console)
}
pub fn setup_storage(storage: &mut Storage, local_drive_spec: &str) -> io::Result<()> {
storage.register_scheme("demos", Box::from(endbasic_repl::demos::DemoDriveFactory::default()));
storage.mount("demos", "demos://").expect("Demos drive shouldn't fail to mount");
storage.register_scheme(
"file",
Box::from(endbasic_std::storage::DirectoryDriveFactory::default()),
);
storage.mount("local", local_drive_spec)?;
storage.cd("local:").expect("Local drive was just registered");
Ok(())
}
async fn run_repl_loop(
console_spec: Option<&str>,
gpio_pins_spec: Option<&str>,
local_drive_spec: &str,
service_url: &str,
) -> Result<i32> {
let mut builder = make_interactive(new_machine_builder(console_spec, gpio_pins_spec)?);
let console = builder.get_console();
let program = builder.get_program();
let storage = builder.get_storage();
setup_storage(&mut storage.borrow_mut(), local_drive_spec)?;
let mut machine = finish_interactive_build(builder, service_url)?;
endbasic_repl::print_welcome(console.clone())?;
endbasic_repl::try_load_autoexec(&mut machine, console.clone(), storage).await?;
Ok(endbasic_repl::run_repl_loop(&mut machine, console, program).await?)
}
async fn run_script<P: AsRef<Path>>(
path: P,
console_spec: Option<&str>,
gpio_pins_spec: Option<&str>,
) -> Result<i32> {
let builder = new_machine_builder(console_spec, gpio_pins_spec)?;
let mut machine = builder.build()?;
let mut input = File::open(path)?;
Ok(machine.exec(&mut input).await?.as_exit_code())
}
async fn run_interactive(
path: &str,
console_spec: Option<&str>,
gpio_pins_spec: Option<&str>,
local_drive_spec: &str,
service_url: &str,
) -> Result<i32> {
let mut builder = make_interactive(new_machine_builder(console_spec, gpio_pins_spec)?);
let console = builder.get_console();
let program = builder.get_program();
let storage = builder.get_storage();
setup_storage(&mut storage.borrow_mut(), local_drive_spec)?;
let mut machine = finish_interactive_build(builder, service_url)?;
match path.strip_prefix("cloud://") {
Some(username_path) => {
let code = endbasic_repl::run_from_cloud(
&mut machine,
console.clone(),
storage.clone(),
program.clone(),
username_path,
false,
)
.await?;
Ok(code)
}
None => {
let mut input = File::open(path)?;
Ok(machine.exec(&mut input).await?.as_exit_code())
}
}
}
fn app_build(builder: Builder) -> Builder {
builder
.copyright("Copyright 2020-2026 Julio Merino")
.license(License::Apache2)
.homepage("https://www.endbasic.dev/")
.bugs("https://github.com/endbasic/endbasic/issues")
.optopt("", "console", "type and properties of the console to use", "CONSOLE-SPEC")
.optopt("", "gpio-pins", "GPIO pins backend to use", "GPIO-PINS-SPEC")
.optflag("i", "interactive", "force interactive mode when running a script")
.optopt("", "local-drive", "location of the drive to mount as LOCAL", "URI")
.optopt("", "service-url", "base URL of the cloud service", "URL")
.trailarg("program-file", 0, 1, "script to execute without entering interactive mode")
.extra_help(app_extra_help)
}
async fn app_main(matches: Matches) -> Result<i32> {
let console_spec = matches.opt_str("console");
let gpio_pins_spec = matches.opt_str("gpio-pins");
let service_url = matches
.opt_str("service-url")
.unwrap_or_else(|| endbasic_client::PROD_API_ADDRESS.to_owned());
match matches.arg_trail() {
[] => {
let local_drive = get_local_drive_spec(matches.opt_str("local-drive"))?;
Ok(run_repl_loop(
console_spec.as_deref(),
gpio_pins_spec.as_deref(),
&local_drive,
&service_url,
)
.await?)
}
[file] => {
if matches.opt_present("interactive") {
let local_drive = get_local_drive_spec(matches.opt_str("local-drive"))?;
Ok(run_interactive(
file,
console_spec.as_deref(),
gpio_pins_spec.as_deref(),
&local_drive,
&service_url,
)
.await?)
} else {
Ok(run_script(file, console_spec.as_deref(), gpio_pins_spec.as_deref()).await?)
}
}
[_, ..] => Err(UsageError::new("Too many arguments").into()),
}
}
tokio_app!("EndBASIC", app_build, app_main);