#![allow(clippy::await_holding_refcell_ref)]
#![allow(clippy::collapsible_else_if)]
#![warn(anonymous_parameters, bad_style, missing_docs)]
#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)]
#![warn(unsafe_code)]
use anyhow::{anyhow, Result};
use async_channel::Sender;
use endbasic_core::exec::Signal;
use endbasic_std::console::Console;
use endbasic_std::storage::Storage;
use getopts::Options;
use std::cell::RefCell;
use std::env;
use std::fs::File;
use std::io;
use std::path::Path;
use std::process;
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 program_name(mut args: env::Args, default_name: &'static str) -> (String, env::Args) {
let name = match args.next() {
Some(arg0) => match Path::new(&arg0).file_stem() {
Some(basename) => match basename.to_str() {
Some(s) => s.to_owned(),
None => default_name.to_owned(),
},
None => default_name.to_owned(),
},
None => default_name.to_owned(),
};
(name, args)
}
fn help(name: &str, opts: &Options) {
let brief = format!("Usage: {} [options] [program-file]", name);
println!("{}", opts.usage(&brief));
println!("CONSOLE-SPEC can be one of the following:");
if cfg!(feature = "sdl") {
println!(" graphics[:SPEC] enables the graphical console and configures it");
println!(" with the settings in SPEC, which is of the form:");
println!(" RESOLUTION,TTF_FONT_PATH,FONT_SIZE");
println!(" individual components of the SPEC can be omitted");
println!(" RESOLUTION can be one of 'fs' (for full screen),");
println!(" 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs'");
}
println!(" text enables the text-based console");
println!();
println!("Report bugs to: https://github.com/endbasic/endbasic/issues");
println!("EndBASIC home page: https://www.endbasic.dev/");
}
fn version() {
println!("EndBASIC {}", env!("CARGO_PKG_VERSION"));
println!("Copyright 2020-2022 Julio Merino");
println!("License Apache Version 2.0 <http://www.apache.org/licenses/LICENSE-2.0>");
}
fn new_machine_builder(console_spec: Option<&str>) -> io::Result<endbasic_std::MachineBuilder> {
#[cfg(feature = "rpi")]
fn add_gpio_pins(builder: endbasic_std::MachineBuilder) -> endbasic_std::MachineBuilder {
builder.with_gpio_pins(Rc::from(RefCell::from(endbasic_rpi::RppalPins::default())))
}
#[cfg(not(feature = "rpi"))]
fn add_gpio_pins(builder: endbasic_std::MachineBuilder) -> endbasic_std::MachineBuilder {
builder
}
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 = add_gpio_pins(builder);
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,
) -> endbasic_core::exec::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_graphics_console(
signals_tx: Sender<Signal>,
spec: &str,
) -> io::Result<Rc<RefCell<dyn Console>>> {
endbasic_sdl::setup(spec, signals_tx)
}
#[cfg(not(feature = "sdl"))]
pub fn setup_graphics_console(
_signals_tx: Sender<Signal>,
_spec: &str,
) -> io::Result<Rc<RefCell<dyn Console>>> {
Err(io::Error::new(io::ErrorKind::InvalidInput, "SDL support not compiled in"))
}
let console: Rc<RefCell<dyn Console>> = match console_spec {
None | Some("text") => setup_text_console(signals_tx)?,
Some("graphics") => setup_graphics_console(signals_tx, "")?,
Some(text) if text.starts_with("graphics:") => {
setup_graphics_console(signals_tx, &text["graphics:".len()..])?
}
Some(text) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid console spec {}", text),
))
}
};
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>,
local_drive_spec: &str,
service_url: &str,
) -> endbasic_core::exec::Result<i32> {
let mut builder = make_interactive(new_machine_builder(console_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>,
) -> endbasic_core::exec::Result<i32> {
let mut machine = new_machine_builder(console_spec)?.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>,
local_drive_spec: &str,
service_url: &str,
) -> endbasic_core::exec::Result<i32> {
let mut builder = make_interactive(new_machine_builder(console_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) => {
endbasic_repl::run_from_cloud(
&mut machine,
console.clone(),
storage.clone(),
program.clone(),
username_path,
false,
)
.await
}
None => {
let mut input = File::open(path)?;
Ok(machine.exec(&mut input).await?.as_exit_code())
}
}
}
async fn safe_main(name: &str, args: env::Args) -> Result<i32> {
let args: Vec<String> = args.collect();
let mut opts = Options::new();
opts.optopt("", "console", "type and properties of the console to use", "CONSOLE-SPEC");
opts.optflag("h", "help", "show command-line usage information and exit");
opts.optflag("i", "interactive", "force interactive mode when running a script");
opts.optopt("", "local-drive", "location of the drive to mount as LOCAL", "URI");
opts.optopt("", "service-url", "base URL of the cloud service", "URL");
opts.optflag("", "version", "show version information and exit");
let matches = opts.parse(args)?;
if matches.opt_present("help") {
help(name, &opts);
return Ok(0);
}
if matches.opt_present("version") {
version();
return Ok(0);
}
let console_spec = matches.opt_str("console");
let service_url = matches
.opt_str("service-url")
.unwrap_or_else(|| endbasic_client::PROD_API_ADDRESS.to_owned());
match matches.free.as_slice() {
[] => {
let local_drive = get_local_drive_spec(matches.opt_str("local-drive"))?;
Ok(run_repl_loop(console_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(), &local_drive, &service_url)
.await?)
} else {
Ok(run_script(file, console_spec.as_deref()).await?)
}
}
[_, ..] => Err(UsageError::new("Too many arguments").into()),
}
}
#[tokio::main]
async fn main() {
let (name, args) = program_name(env::args(), "endbasic");
let exit_code = match safe_main(&name, args).await {
Ok(code) => code,
Err(e) => {
if let Some(e) = e.downcast_ref::<UsageError>() {
eprintln!("Usage error: {}", e);
eprintln!("Type {} --help for more information", name);
2
} else if let Some(e) = e.downcast_ref::<getopts::Fail>() {
eprintln!("Usage error: {}", e);
eprintln!("Type {} --help for more information", name);
2
} else {
eprintln!("{}: {}", name, e);
1
}
}
};
process::exit(exit_code);
}