Skip to main content

endbasic/
lib.rs

1// EndBASIC
2// Copyright 2026 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Supporting code for EndBASIC's CLI.
18
19use anyhow::Result;
20use async_channel::{Receiver, Sender};
21use endbasic_client::CloudService;
22use endbasic_std::console::ConsoleFactory;
23use endbasic_std::storage::Storage;
24use endbasic_std::{Error as StdError, MachineBuilder, Signal};
25use getoptsargs::prelude::*;
26use std::cell::RefCell;
27use std::fs::File;
28use std::path::Path;
29use std::rc::Rc;
30
31mod console;
32pub use console::setup_console;
33
34mod gpio;
35use gpio::setup_gpio_pins;
36
37mod propline;
38pub use propline::{PropLine, extract_propline};
39
40mod storage;
41use storage::{get_local_drive_spec, setup_storage};
42
43/// Exit code representing `SIGINT` following shell semantics.
44const INTERRUPTED_EXIT_CODE: i32 = 128 + 2;
45
46/// Creates a new EndBASIC machine builder based on the features enabled in this crate.
47fn new_machine_builder(
48    console_factory: Box<dyn ConsoleFactory>,
49    signals_chan: (Sender<Signal>, Receiver<Signal>),
50    gpio_pins_spec: Option<&str>,
51) -> Result<MachineBuilder> {
52    let console = console_factory.build()?;
53    let mut builder = MachineBuilder::default();
54    builder = builder.with_console(console);
55    builder = builder.with_signals_chan(signals_chan);
56    builder = builder.with_gpio_pins(setup_gpio_pins(gpio_pins_spec)?);
57    Ok(builder)
58}
59
60/// Enters the interactive interpreter.
61///
62/// `local_drive` is the optional local drive to mount and use as the default location.
63/// `service_url` is the base URL of the cloud service.
64pub async fn run_repl_loop(
65    console_factory: Box<dyn ConsoleFactory>,
66    signals_chan: (Sender<Signal>, Receiver<Signal>),
67    gpio_pins_spec: Option<&str>,
68    local_drive_spec: &str,
69    service_url: &str,
70) -> Result<i32> {
71    let mut builder = new_machine_builder(console_factory, signals_chan, gpio_pins_spec)?;
72    let console = builder.get_console();
73    let program = Rc::from(RefCell::from(endbasic_repl::editor::Editor::default()));
74    let storage = Rc::from(RefCell::from(Storage::default()));
75    setup_storage(&mut storage.borrow_mut(), local_drive_spec)?;
76
77    let service = Rc::from(RefCell::from(CloudService::new(service_url)?));
78    endbasic_client::add_all(
79        &mut builder,
80        service,
81        console.clone(),
82        storage.clone(),
83        "https://repl.endbasic.dev/",
84    );
85
86    let mut machine = builder
87        .make_interactive()
88        .with_program(program.clone())
89        .with_storage(storage.clone())
90        .build();
91    endbasic_repl::print_welcome(&mut *console.borrow_mut())?;
92    endbasic_repl::try_load_autoexec(&mut machine, console.clone(), storage).await?;
93    Ok(endbasic_repl::run_repl_loop(&mut machine, console, program).await?)
94}
95
96/// Executes the `path` program in a fresh machine.
97pub async fn run_script<P: AsRef<Path>>(
98    path: P,
99    console_factory: Box<dyn ConsoleFactory>,
100    signals_chan: (Sender<Signal>, Receiver<Signal>),
101    gpio_pins_spec: Option<&str>,
102) -> Result<i32> {
103    let builder = new_machine_builder(console_factory, signals_chan, gpio_pins_spec)?;
104    let mut machine = builder.build();
105    let mut input = File::open(path)?;
106
107    machine.compile(&mut input)?;
108    match machine.exec().await {
109        Ok(Some(code)) => Ok(code),
110        Ok(None) => Ok(0),
111        Err(StdError::Break) => Ok(INTERRUPTED_EXIT_CODE),
112        Err(e) => Err(e.into()),
113    }
114}
115
116/// Executes the `path` program in a fresh machine allowing any interactive-only calls.
117///
118/// `local_drive` is the optional local drive to mount and use as the default location.
119/// `service_url` is the base URL of the cloud service.
120///
121/// If `path` starts with `cloud://`, this uses the same auto-run features that the web UI
122/// exposes.  The presence of this here is kind of a hack but avoids having too much logic
123/// just in the web and helps test this feature.
124pub async fn run_interactive(
125    path: &str,
126    console_factory: Box<dyn ConsoleFactory>,
127    signals_chan: (Sender<Signal>, Receiver<Signal>),
128    gpio_pins_spec: Option<&str>,
129    local_drive_spec: &str,
130    service_url: &str,
131) -> Result<i32> {
132    let mut builder = new_machine_builder(console_factory, signals_chan, gpio_pins_spec)?;
133    let console = builder.get_console();
134    let program = Rc::from(RefCell::from(endbasic_repl::editor::Editor::default()));
135    let storage = Rc::from(RefCell::from(Storage::default()));
136    setup_storage(&mut storage.borrow_mut(), local_drive_spec)?;
137
138    let service = Rc::from(RefCell::from(CloudService::new(service_url)?));
139    endbasic_client::add_all(
140        &mut builder,
141        service,
142        console.clone(),
143        storage.clone(),
144        "https://repl.endbasic.dev/",
145    );
146
147    let mut machine = builder
148        .make_interactive()
149        .with_program(program.clone())
150        .with_storage(storage.clone())
151        .build();
152
153    match path.strip_prefix("cloud://") {
154        Some(username_path) => {
155            let path =
156                endbasic_repl::mount_cloud_share(console.clone(), storage.clone(), username_path)?;
157            let code = endbasic_repl::run_from_storage_path(
158                &mut machine,
159                console.clone(),
160                storage.clone(),
161                program.clone(),
162                &path,
163                false,
164            )
165            .await?;
166            Ok(code)
167        }
168        None => {
169            let mut input = File::open(path)?;
170            machine.compile(&mut input)?;
171            match machine.exec().await {
172                Ok(Some(code)) => Ok(code),
173                Ok(None) => Ok(0),
174                Err(StdError::Break) => Ok(INTERRUPTED_EXIT_CODE),
175                Err(e) => Err(e.into()),
176            }
177        }
178    }
179}
180
181/// Representation of the CLI execution mode and the arguments required for each.
182pub enum AppMode {
183    /// Enter the REPL.
184    ///
185    /// The first argument contains the local drive spec.
186    Repl(String),
187
188    /// Run a file as if it had invoked in a REPL context.
189    ///
190    /// The first argument contains the file to run and the second argument contains the local
191    /// drive spec.
192    RunInteractive(String, String),
193
194    /// Run a file in scripting mode.
195    ///
196    /// The first argument contains the file to run.
197    RunScript(String),
198}
199
200impl AppMode {
201    /// Determines the app mode by parsing the remainder of `matches`.
202    pub fn from_matches(matches: Matches) -> Result<AppMode> {
203        match matches.arg_trail() {
204            [] => {
205                let local_drive = get_local_drive_spec(matches.opt_str("local-drive"))?;
206                Ok(AppMode::Repl(local_drive))
207            }
208            [file] => {
209                if matches.opt_present("interactive") {
210                    let local_drive = get_local_drive_spec(matches.opt_str("local-drive"))?;
211                    Ok(AppMode::RunInteractive((*file).to_owned(), local_drive))
212                } else {
213                    Ok(AppMode::RunScript((*file).to_owned()))
214                }
215            }
216            [_, ..] => Err(bad_usage!("Too many arguments").into()),
217        }
218    }
219}
220
221/// Runs the EndBASIC CLI depending on the provided `app_mode`.
222pub async fn async_app_main(
223    app_mode: AppMode,
224    console_factory: Box<dyn ConsoleFactory>,
225    signals_chan: (Sender<Signal>, Receiver<Signal>),
226    gpio_pins_spec: Option<String>,
227    service_url: String,
228) -> Result<i32> {
229    match app_mode {
230        AppMode::Repl(local_drive) => Ok(run_repl_loop(
231            console_factory,
232            signals_chan,
233            gpio_pins_spec.as_deref(),
234            &local_drive,
235            &service_url,
236        )
237        .await?),
238        AppMode::RunInteractive(file, local_drive) => Ok(run_interactive(
239            &file,
240            console_factory,
241            signals_chan,
242            gpio_pins_spec.as_deref(),
243            &local_drive,
244            &service_url,
245        )
246        .await?),
247        AppMode::RunScript(file) => {
248            Ok(run_script(file, console_factory, signals_chan, gpio_pins_spec.as_deref()).await?)
249        }
250    }
251}