diff options
Diffstat (limited to 'src/bin')
-rw-r--r-- | src/bin/br.rs | 521 | ||||
-rw-r--r-- | src/bin/br/config.rs | 122 | ||||
-rw-r--r-- | src/bin/br/load.rs | 81 | ||||
-rw-r--r-- | src/bin/br/main.rs | 233 |
4 files changed, 436 insertions, 521 deletions
diff --git a/src/bin/br.rs b/src/bin/br.rs deleted file mode 100644 index 431ae39..0000000 --- a/src/bin/br.rs +++ /dev/null @@ -1,521 +0,0 @@ -use bedrock_asm::*; -use bedrock_pc::*; -use phosphor::*; - -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; -use std::process::exit; - - -const NORMAL: &str = "\x1b[0m"; -const BOLD: &str = "\x1b[1m"; -const WHITE: &str = "\x1b[37m"; -const RED: &str = "\x1b[31m"; -const BLUE: &str = "\x1b[34m"; - -static mut VERBOSE: bool = false; - -macro_rules! verbose { - ($($tokens:tt)*) => { if unsafe { VERBOSE } { - eprint!("{BOLD}{BLUE}[INFO]{NORMAL}: "); eprint!($($tokens)*); - eprintln!("{NORMAL}"); - } }; -} -macro_rules! error { - ($($tokens:tt)*) => {{ - eprint!("{BOLD}{RED}[ERROR]{WHITE}: "); eprint!($($tokens)*); - eprintln!("{NORMAL}"); - }}; -} - - -fn main() { - let args = Arguments::from_env_or_exit(); - if args.version { - let name = env!("CARGO_PKG_NAME"); - let version = env!("CARGO_PKG_VERSION"); - eprintln!("{name} v{version}"); - eprintln!("Written by Ben Bridle."); - std::process::exit(0); - } - if args.verbose { - unsafe { VERBOSE = true; } - } - match args.subcommand { - ArgumentsCmd::Run(run) => main_run(run), - ArgumentsCmd::Asm(asm) => main_asm(asm), - } -} - -fn main_run(args: Run) { - let program_path = args.program.as_ref().map(|p| p.as_path()); - let Bytecode { bytes: bytecode, path } = load_bytecode(program_path); - let symbols_path = path.as_ref().map(|p| { - let mut path = p.to_path_buf(); - path.set_extension("br.sym"); - path - }); - - let metadata = parse_metadata(&bytecode); - if metadata.is_none() { - verbose!("Could not read program metadata"); - } - - let mut config = EmulatorConfig { - dimensions: ScreenDimensions::ZERO, - fullscreen: args.fullscreen, - scale: args.scale(), - debug_palette: args.palette(), - show_cursor: args.show_cursor, - initial_transmission: None, - decode_stdin: args.decode_stdin, - encode_stdout: args.encode_stdout, - symbols_path, - }; - let phosphor = Phosphor::new(); - - if phosphor.is_ok() && args.dimensions().is_some() { - verbose!("Starting graphical emulator"); - let mut phosphor = phosphor.unwrap(); - config.dimensions = args.dimensions().unwrap(); - let cursor = match config.show_cursor { - true => Some(CursorIcon::Default), - false => None, - }; - - let mut graphical = GraphicalEmulator::new(&config, args.debug, unsafe {VERBOSE}); - graphical.load_program(&bytecode); - if let EmulatorSignal::Promote = graphical.run() { - let program_name = match &metadata { - Some(metadata) => match &metadata.name { - Some(name) => name.to_string(), - None => String::from("Bedrock"), - } - None => String::from("Bedrock"), - }; - let window = WindowBuilder { - dimensions: Some(graphical.dimensions()), - size_bounds: Some(graphical.size_bounds()), - fullscreen: graphical.fullscreen, - scale: graphical.scale, - title: Some(program_name), - cursor, - icon: None, - program: Box::new(graphical), - }; - - phosphor.create_window(window); - phosphor.run().unwrap(); - } - } else { - verbose!("Starting headless emulator"); - let mut headless = HeadlessEmulator::new(&config, args.debug, unsafe {VERBOSE}); - headless.load_program(&bytecode); - headless.run(args.debug); - }; - - std::process::exit(0); -} - -fn load_bytecode(path: Option<&Path>) -> Bytecode { - // TODO: Etch file location into bytecode. - if let Some(path) = path { - if let Ok(bytecode) = load_bytecode_from_file(path) { - let length = bytecode.bytes.len(); - let path = bytecode.path(); - verbose!("Loaded program from {path:?} ({length} bytes)"); - return bytecode; - } else if let Some(bytecode) = load_bytecode_from_bedrock_path(path) { - let length = bytecode.bytes.len(); - let path = bytecode.path(); - verbose!("Loaded program from {path:?} ({length} bytes)"); - return bytecode; - } else { - error!("Could not read program from {path:?}, exiting"); - exit(1); - } - } else { - verbose!("Reading program from standard input..."); - if let Ok(bytecode) = load_bytecode_from_stdin() { - let length = bytecode.bytes.len(); - verbose!("Loaded program from standard input ({length} bytes)"); - return bytecode; - } else { - error!("Could not read program from standard input, exiting"); - exit(1); - } - } -} - -/// Attempt to load bytecode from a directory in the BEDROCK_PATH environment variable. -fn load_bytecode_from_bedrock_path(path: &Path) -> Option<Bytecode> { - if path.is_relative() && path.components().count() == 1 { - for base_path in std::env::var("BEDROCK_PATH").ok()?.split(':') { - let mut base_path = PathBuf::from(base_path); - if !base_path.is_absolute() { continue; } - base_path.push(path); - verbose!("Attempting to load program from {base_path:?}"); - if let Ok(bytecode) = load_bytecode_from_file(&base_path) { - return Some(bytecode); - } - if path.extension().is_some() { continue; } - base_path.set_extension("br"); - verbose!("Attempting to load program from {base_path:?}"); - if let Ok(bytecode) = load_bytecode_from_file(&base_path) { - return Some(bytecode); - } - } - } - return None; -} - -/// Attempt to load bytecode from a file path. -fn load_bytecode_from_file(path: &Path) -> Result<Bytecode, std::io::Error> { - // Canonicalize paths so that symbolic links to program files resolve to - // the real program directory, which could contain a symbols file. - let path = match path.canonicalize() { - Ok(canonical) => canonical, - Err(_) => path.to_path_buf(), - }; - load_bytecode_from_readable_source(std::fs::File::open(&path)?, Some(&path)) -} - -/// Attempt to load bytecode from standard input. -fn load_bytecode_from_stdin() -> Result<Bytecode, std::io::Error> { - load_bytecode_from_readable_source(std::io::stdin(), None) -} - -/// Attempt to load bytecode from a source that implements std::io::Read. -fn load_bytecode_from_readable_source(source: impl Read, path: Option<&Path>) -> Result<Bytecode, std::io::Error> { - let mut bytes = Vec::<u8>::new(); - source.take(65536).read_to_end(&mut bytes)?; - return Ok(Bytecode { bytes, path: path.map(|p| p.to_path_buf()) }); -} - -struct Bytecode { - bytes: Vec<u8>, - path: Option<PathBuf>, -} - -impl Bytecode { - fn path(&self) -> String { - match &self.path { - Some(path) => path.as_os_str().to_string_lossy().to_string(), - None => String::from("<unknown>"), - } - } -} - - -fn main_asm(args: Asm) { - // ----------------------------------------------------------------------- - // RESOLVE syntactic symbols - let ext = args.ext.unwrap_or(String::from("brc")); - let source_path = args.source.clone().map(|p| { - p.canonicalize().unwrap_or(p) - }); - - let mut resolver = if let Some(path) = &source_path { - match SourceUnit::from_path(&path, &ext) { - Ok(source_unit) => SymbolResolver::from_source_unit(source_unit), - Err(err) => { - match err { - ParseError::InvalidExtension => error!( - "File {path:?} has invalid extension, must be '.{ext}'"), - ParseError::NotFound => error!( - "File {path:?} was not found"), - ParseError::InvalidUtf8 => error!( - "File {path:?} does not contain valid UTF-8 text"), - ParseError::NotReadable => error!( - "File {path:?} is not readable"), - ParseError::IsADirectory => error!( - "File {path:?} is a directory"), - ParseError::Unknown => error!( - "Unknown error while attempting to read from {path:?}") - }; - exit(1); - } - } - } else { - let mut source_code = String::new(); - verbose!("Reading program source from standard input"); - if let Err(err) = std::io::stdin().read_to_string(&mut source_code) { - error!("Could not read from standard input"); - eprintln!("{err:?}"); - exit(1); - } - let path = "<standard input>"; - let source_unit = SourceUnit::from_source_code(source_code, path); - SymbolResolver::from_source_unit(source_unit) - }; - // Load project libraries. - if let Some(path) = &source_path { - if !args.no_libs && !args.no_project_libs { - let project_library = gather_project_libraries(path, &ext); - resolver.add_library_units(project_library); - } - } - // Load environment libraries. - if !args.no_libs && !args.no_env_libs { - for env_library in gather_environment_libraries(&ext) { - resolver.add_library_units(env_library); - } - } - resolver.resolve(); - - // ----------------------------------------------------------------------- - // PRINT information, generate merged source code - if args.tree { - print_source_tree(&resolver); - } - if print_resolver_errors(&resolver) { - std::process::exit(1); - }; - let merged_source = match resolver.get_merged_source_code() { - Ok(merged_source) => merged_source, - Err(ids) => { - print_cyclic_source_units(&ids, &resolver); - std::process::exit(1); - }, - }; - if args.resolve && !args.check { - write_bytes_and_exit(merged_source.as_bytes(), args.output.as_ref()); - } - - // ----------------------------------------------------------------------- - // PARSE semantic tokens from merged source code - let path = Some("<merged source>"); - let mut semantic_tokens = generate_semantic_tokens(&merged_source, path); - if print_semantic_errors(&semantic_tokens, &merged_source) { - std::process::exit(1); - }; - - // ----------------------------------------------------------------------- - // GENERATE symbols file and bytecode - let bytecode = generate_bytecode(&mut semantic_tokens); - - if args.symbols && !args.check { - if let Some(path) = &args.output { - let mut symbols_path = path.to_path_buf(); - symbols_path.set_extension("br.sym"); - let symbols = generate_symbols_file(&semantic_tokens); - if let Err(err) = std::fs::write(&symbols_path, symbols) { - verbose!("Could not write to symbols path {symbols_path:?}"); - eprintln!("{err:?}"); - } else { - verbose!("Saved debug symbols to {symbols_path:?}"); - } - } - } - - let length = bytecode.len(); - let percentage = (length as f32 / 65536.0 * 100.0).round() as u16; - verbose!("Assembled program in {length} bytes ({percentage}% of maximum)"); - - if !args.check { - write_bytes_and_exit(&bytecode, args.output.as_ref()); - } -} - -fn write_bytes_and_exit<P: AsRef<Path>>(bytes: &[u8], path: Option<&P>) -> ! { - if let Some(path) = path { - if let Err(err) = std::fs::write(path, bytes) { - error!("Could not write to path {:?}", path.as_ref()); - eprintln!("{err:?}"); - exit(1); - } - } else { - if let Err(err) = std::io::stdout().write_all(bytes) { - error!("Could not write to standard output"); - eprintln!("{err:?}"); - exit(1); - } - } - exit(0); -} - - -xflags::xflags! { - /// Integrated Bedrock assembler and emulator. - /// - /// The arguments and options shown are for running a Bedrock program. - /// To see the documentation for the assembler, run `br asm --help` - /// - /// Usage: - /// To load a Bedrock program from a file, run `br [path]`, where - /// [path] is the path of an assembled Bedrock program. - /// - /// To load a Bedrock program from piped input, run `[command] | br`, - /// where [command] is a command that generates Bedrock bytecode. - /// - /// To assemble a Bedrock program from a source file and write to an - /// output file, run `br asm [source] [output]`, where [source] is the - /// path of the source file and [output] is the path to write to. - /// - /// To assemble and run a Bedrock program without saving to a file, - /// run `br asm [source] | br`, where [source] is the path of the - /// source file. - /// - /// Environment variables: - /// BEDROCK_PATH - /// A list of colon-separated paths which will be searched to find - /// a Bedrock program when the program doesn't exist in the current - /// directory. This allows the user to run frequently-used programs - /// from any directory. - /// BEDROCK_LIBS - /// A list of colon-separated paths which will be searched to find - /// Bedrock source code files to use as libraries when assembling a - /// Bedrock program. If a library file resolves an unresolved symbol - /// in the program being assembled, the library file will be merged - /// into the program. - cmd arguments { - /// Print additional debug information - optional -v, --verbose - /// Print the assembler version and exit - optional --version - - /// Run a Bedrock program (this is the default command) - default cmd run { - /// Path to a Bedrock program to run - optional program: PathBuf - - /// Show debug information while the program is running - optional -d, --debug - /// Start the program in fullscreen mode (toggle with F11) - optional -f, --fullscreen - /// Set the initial window size in the format <width>x<height> - optional -s, --screen size: ParsedDimensions - /// Set the pixel size for the screen (change with F5/F6) - optional -z, --zoom factor: u32 - /// Set a debug colour palette in the format <rgb>,... (toggle with F2) - optional --palette colours: ParsedPalette - /// Show the operating system cursor over the window - optional --show-cursor - /// Decode standard input - optional -i, --decode-stdin - /// Encode standard output - optional -o, --encode-stdout - } - - /// Assemble a Bedrock program from source code - cmd asm { - /// Bedrock source code file to assemble. - optional source: PathBuf - /// Destination path for assembler output. - optional output: PathBuf - /// File extension to identify source files. - optional ext: String - - /// Don't include libraries or resolve references. - optional --no-libs - /// Don't include project libraries - optional --no-project-libs - /// Don't include environment libraries. - optional --no-env-libs - - /// Show the resolved source file heirarchy - optional --tree - /// Assemble the program without saving any output - optional --check - /// Only return resolved source code. - optional --resolve - /// Generate debug symbols with file extension '.br.sym' - optional --symbols - } - } -} - - -impl Run { - pub fn dimensions(&self) -> Option<geometry::Dimensions<u16>> { - let size = match &self.screen { - Some(parsed) => geometry::Dimensions::new(parsed.width, parsed.height), - None => DEFAULT_SCREEN_SIZE / (self.scale() as u16), - }; - match size.is_zero() { - true => None, - false => Some(size), - } - } - - pub fn scale(&self) -> u32 { - std::cmp::max(1, self.zoom.unwrap_or(1)) - } - - pub fn palette(&self) -> Option<[Colour; 16]> { - self.palette.as_ref().map(|p| p.palette) - } -} - - -#[derive(Debug)] -struct ParsedDimensions { width: u16, height: u16 } -impl std::str::FromStr for ParsedDimensions { - type Err = ParsedDimensionsError; - fn from_str(s: &str) -> Result<Self, Self::Err> { - if s == "none" { - Ok( Self { width: 0, height: 0 } ) - } else { - let (w_str, h_str) = s.split_once('x').ok_or(ParsedDimensionsError)?; - Ok( Self { - width: u16::from_str(w_str).or(Err(ParsedDimensionsError))?, - height: u16::from_str(h_str).or(Err(ParsedDimensionsError))?, - } ) - } - } -} - -pub struct ParsedDimensionsError; -impl std::fmt::Display for ParsedDimensionsError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - f.write_str("try giving a value like 800x600") - } -} - - -#[derive(Debug)] -pub struct ParsedPalette { palette: [Colour; 16] } -impl std::str::FromStr for ParsedPalette { - type Err = ParsedPaletteError; - fn from_str(s: &str) -> Result<Self, Self::Err> { - fn decode_ascii_hex_digit(ascii: u8) -> Result<u8, ParsedPaletteError> { - match ascii { - b'0'..=b'9' => Ok(ascii - b'0'), - b'a'..=b'f' => Ok(ascii - b'a' + 10), - b'A'..=b'F' => Ok(ascii - b'A' + 10), - _ => { Err(ParsedPaletteError)} - } - } - let mut colours = Vec::new(); - for token in s.split(',') { - let mut bytes = token.bytes(); - if bytes.len() != 3 { return Err(ParsedPaletteError); } - let r = decode_ascii_hex_digit(bytes.next().unwrap())?; - let g = decode_ascii_hex_digit(bytes.next().unwrap())?; - let b = decode_ascii_hex_digit(bytes.next().unwrap())?; - colours.push(Colour::from_rgb(r*17, g*17, b*17)); - } - let c = &colours; - let palette = match colours.len() { - 2 => [ c[0], c[1], c[0], c[1], c[0], c[1], c[0], c[1], - c[0], c[1], c[0], c[1], c[0], c[1], c[0], c[1] ], - 3 => [ c[0], c[1], c[0], c[2], c[0], c[1], c[0], c[2], - c[0], c[1], c[0], c[2], c[0], c[1], c[0], c[2] ], - 4 => [ c[0], c[1], c[2], c[3], c[0], c[1], c[2], c[3], - c[0], c[1], c[2], c[3], c[0], c[1], c[2], c[3] ], - 8 => [ c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], - c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7] ], - 16 => [ c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], - c[8], c[9], c[10], c[11], c[12], c[13], c[14], c[15] ], - _ => return Err(ParsedPaletteError), - }; - Ok( Self { palette }) - } -} - -pub struct ParsedPaletteError; -impl std::fmt::Display for ParsedPaletteError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - f.write_str("try giving a value like 000,fff,880,808") - } -} diff --git a/src/bin/br/config.rs b/src/bin/br/config.rs new file mode 100644 index 0000000..56d5190 --- /dev/null +++ b/src/bin/br/config.rs @@ -0,0 +1,122 @@ +use crate::*; + + +#[derive(Copy, Clone, PartialEq)] +pub enum Mode { + Graphical, + Headless, + Dynamic, +} + +impl Mode { + pub fn from_str(string: &str) -> Self { + match string { + "g" | "graphical" => Self::Graphical, + "h" | "headless" => Self::Headless, + "d" | "dynamic" => Self::Dynamic, + _ => fatal!("Invalid mode string '{string}'"), + } + } +} + + +pub fn parse_dimensions(string: &str) -> ScreenDimensions { + fn parse_inner(string: &str) -> Option<ScreenDimensions> { + let (w_str, h_str) = string.trim().split_once('x')?; + Some( ScreenDimensions { + width: w_str.parse().ok()?, + height: h_str.parse().ok()?, + } ) + } + let dimensions = parse_inner(string).unwrap_or_else(|| { + fatal!("Invalid dimensions string '{string}'"); + }); + if dimensions.is_zero() { + fatal!("Screen dimensions must be greater than zero"); + } + return dimensions; +} + + +pub fn parse_palette(string: &str) -> [Colour; 16] { + fn decode_ascii_hex_digit(ascii: u8) -> Option<u8> { + match ascii { + b'0'..=b'9' => Some(ascii - b'0'), + b'a'..=b'f' => Some(ascii - b'a' + 10), + b'A'..=b'F' => Some(ascii - b'A' + 10), + _ => { None } + } + } + fn parse_inner(string: &str) -> Option<[Colour; 16]> { + let mut c = Vec::new(); + for token in string.split(',') { + let mut bytes = token.bytes(); + if bytes.len() != 3 { return None; } + let r = decode_ascii_hex_digit(bytes.next().unwrap())?; + let g = decode_ascii_hex_digit(bytes.next().unwrap())?; + let b = decode_ascii_hex_digit(bytes.next().unwrap())?; + c.push(Colour::from_rgb(r*17, g*17, b*17)); + } + Some(match c.len() { + 2 => [ c[0], c[1], c[0], c[1], c[0], c[1], c[0], c[1], + c[0], c[1], c[0], c[1], c[0], c[1], c[0], c[1] ], + 3 => [ c[0], c[1], c[0], c[2], c[0], c[1], c[0], c[2], + c[0], c[1], c[0], c[2], c[0], c[1], c[0], c[2] ], + 4 => [ c[0], c[1], c[2], c[3], c[0], c[1], c[2], c[3], + c[0], c[1], c[2], c[3], c[0], c[1], c[2], c[3] ], + 8 => [ c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], + c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7] ], + 16 => [ c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], + c[8], c[9], c[10], c[11], c[12], c[13], c[14], c[15] ], + _ => return None, + }) + } + parse_inner(string).unwrap_or_else(|| { + fatal!("Invalid palette string '{string}'"); + }) +} + + +pub fn parse_metadata_colour(colour: Option<MetadataColour>) -> Option<Colour> { + let c = colour?; + Some(Colour::from_rgb(c.red, c.green, c.blue)) +} + + +pub fn parse_small_icon(bytes: Option<Vec<u8>>, bg: Colour, fg: Colour) -> Option<Icon> { + let rgba = sprite_data_to_rgb(&bytes?, 3, bg, fg); + match Icon::from_rgba(rgba, 24, 24) { + Ok(icon) => Some(icon), + Err(err) => unreachable!("Error while parsing small icon data: {err}"), + } +} + +pub fn parse_large_icon(bytes: Option<Vec<u8>>, bg: Colour, fg: Colour) -> Option<Icon> { + let rgba = sprite_data_to_rgb(&bytes?, 8, bg, fg); + match Icon::from_rgba(rgba, 64, 64) { + Ok(icon) => Some(icon), + Err(err) => unreachable!("Error while parsing large icon data: {err}"), + } +} + +fn sprite_data_to_rgb(bytes: &[u8], size: usize, bg: Colour, fg: Colour) -> Vec<u8> { + let sprites: Vec<&[u8]> = bytes.chunks_exact(8).collect(); + let mut rgba = Vec::new(); + for sprite_row in 0..size { + for pixel_row in 0..8 { + for sprite_column in 0..size { + let sprite = &sprites[sprite_column + (sprite_row * size)]; + let row = &sprite[pixel_row]; + for bit in 0..8 { + let state = row & (0x80 >> bit); + let colour = match state != 0 { + true => fg, + false => bg, + }; + rgba.extend_from_slice(&colour.as_rgba_array()); + } + } + } + } + return rgba; +} diff --git a/src/bin/br/load.rs b/src/bin/br/load.rs new file mode 100644 index 0000000..93a748c --- /dev/null +++ b/src/bin/br/load.rs @@ -0,0 +1,81 @@ +use crate::*; + +use std::io::Read; +use std::path::{Path, PathBuf}; + + +pub struct LoadedProgram { + pub bytecode: Vec<u8>, + pub path: Option<PathBuf>, +} + +/// Load program from path or standard input. +pub fn load_program(path: Option<&PathBuf>) -> LoadedProgram { + if let Some(path) = path { + if let Ok(program) = load_program_from_file(path) { + let length = program.bytecode.len(); + info!("Loaded program from {path:?} ({length} bytes)"); + return program; + } else if let Some(program) = load_program_from_env(path) { + let length = program.bytecode.len(); + info!("Loaded program from {path:?} ({length} bytes)"); + return program; + } else { + fatal!("Could not read program from {path:?}"); + } + } else { + info!("Reading program from standard input..."); + if let Ok(program) = load_program_from_stdin() { + let length = program.bytecode.len(); + info!("Loaded program from standard input ({length} bytes)"); + return program; + } else { + fatal!("Could not read program from standard input"); + } + } +} + +/// Attempt to load program from a directory in the BEDROCK_PATH environment variable. +fn load_program_from_env(path: &Path) -> Option<LoadedProgram> { + // Check that the path is a bare program name. + if path.is_relative() && path.components().count() == 1 { + for base_path in std::env::var("BEDROCK_PATH").ok()?.split(':') { + let mut base_path = PathBuf::from(base_path); + // Skip relative paths. + if !base_path.is_absolute() { continue; } + base_path.push(path); + if let Ok(program) = load_program_from_file(&base_path) { + return Some(program); + } + if path.extension().is_some() { continue; } + base_path.set_extension("br"); + if let Ok(program) = load_program_from_file(&base_path) { + return Some(program); + } + } + } + return None; +} + +/// Attempt to load program from a file path. +fn load_program_from_file(path: &Path) -> Result<LoadedProgram, std::io::Error> { + // Canonicalize paths so that symbolic links to program files resolve to + // the real program directory, which could contain a symbols file. + let path = match path.canonicalize() { + Ok(canonical) => canonical, + Err(_) => path.to_path_buf(), + }; + load_program_from_readable_source(std::fs::File::open(&path)?, Some(&path)) +} + +/// Attempt to load program from standard input. +fn load_program_from_stdin() -> Result<LoadedProgram, std::io::Error> { + load_program_from_readable_source(std::io::stdin(), None) +} + +/// Attempt to load program from a source that implements std::io::Read. +fn load_program_from_readable_source(source: impl Read, path: Option<&Path>) -> Result<LoadedProgram, std::io::Error> { + let mut bytecode = Vec::<u8>::new(); + source.take(65536).read_to_end(&mut bytecode)?; + return Ok(LoadedProgram { bytecode, path: path.map(|p| p.to_path_buf()) }); +} diff --git a/src/bin/br/main.rs b/src/bin/br/main.rs new file mode 100644 index 0000000..da11a18 --- /dev/null +++ b/src/bin/br/main.rs @@ -0,0 +1,233 @@ +#![feature(path_add_extension)] + +mod config; +mod load; +pub use config::*; +pub use load::*; + +use bedrock_asm::*; +use bedrock_core::*; +use bedrock_pc::*; +use log::*; +use switchboard::*; +use phosphor::*; + +use std::cmp::{min, max}; +use std::num::NonZeroU32; + + +fn main() { + let mut args = Switchboard::from_env(); + if let Some("asm") = args.peek() { + args.pop(); + assemble(args, "br asm"); + } + + // ----------------------------------------------------------------------- + + args.named("help").short('h'); + args.named("version"); + args.named("verbose").short('v'); + + if args.get("help").as_bool() { + print_help(); + std::process::exit(0); + } + if args.get("version").as_bool() { + let name = env!("CARGO_PKG_NAME"); + let version = env!("CARGO_PKG_VERSION"); + eprintln!("{name} v{version}"); + eprintln!("Written by Ben Bridle."); + std::process::exit(0); + } + if args.get("verbose").as_bool() { + log::set_log_level(log::LogLevel::Info); + } + + args.positional("source"); + args.named("debug").short('d'); + args.named("mode").default("dynamic"); + + args.named("palette"); + args.named("fullscreen").short('f'); + args.named("show-cursor").short('c'); + args.named("zoom").short('z').quick("3").default("1"); + args.named("size").short('s'); + args.named("decode-stdin").short('i'); + args.named("encode-stdout").short('o'); + args.named("trust-files"); + args.raise_errors(); + + let source = args.get("source").as_path_opt(); + let debug = args.get("debug").as_bool(); + let mode = Mode::from_str(args.get("mode").as_str()); + let palette = args.get("palette").as_str_opt().map(parse_palette); + let fullscreen = args.get("fullscreen").as_bool(); + let show_cursor = args.get("show-cursor").as_bool(); + let zoom_raw = min(10, max(1, args.get("zoom").as_u32())); + let zoom = unsafe { NonZeroU32::new_unchecked(zoom_raw) }; + let dimensions = match args.get("size").as_str_opt() { + Some(string) => parse_dimensions(string), + None => DEFAULT_SCREEN_SIZE / (zoom_raw as u16), + }; + let decode_stdin = args.get("decode-stdin").as_bool(); + let encode_stdout = args.get("encode-stdout").as_bool(); + let trust_files = args.get("trust-files").as_bool(); + + // ----------------------------------------------------------------------- + + let LoadedProgram { bytecode, path } = load_program(source.as_ref()); + let mut title = String::from("Bedrock program"); + let mut icon = None; + + let metadata = Metadata::from(&bytecode); + + if let Some(ref metadata) = metadata { + let name = metadata.name().unwrap_or("unnamed".to_string()); + let authors = metadata.authors().unwrap_or_else(Vec::new); + let mut metadata_string = format!("Program is '{name}'"); + if authors.len() > 0 { + metadata_string.push_str(&format!(", by {}", authors[0])); } + if authors.len() > 1 { + metadata_string.push_str(" and others"); } + info!("{metadata_string}"); + + match name.split_once('/') { + Some((name, _version)) if !name.is_empty() => title = name.to_string(), + _ => title = name.to_string(), + } + let bg = parse_metadata_colour(metadata.bg_colour()).unwrap_or(Colour::rgb(0x1E1C26)); + let fg = parse_metadata_colour(metadata.fg_colour()).unwrap_or(Colour::rgb(0xED614F)); + match parse_large_icon(metadata.large_icon(), bg, fg) { + Some(large_icon) => icon = Some(large_icon), + None => match parse_small_icon(metadata.small_icon(), bg, fg) { + Some(small_icon) => icon = Some(small_icon), + None => (), + } + } + } else { + info!("Program does not contain metadata"); + } + + let symbols_path = path.as_ref().map(|p| { + let mut path = p.to_path_buf(); + path.add_extension("sym"); path + }); + + let name = metadata.and_then(|m| m.name()).and_then(|n| match n.split_once('/') { + Some((name, _)) => Some(name.to_string()), + None => Some(n), + }); + let identifier = name.as_ref().and_then( + |n| Some(n.to_lowercase().chars().filter_map( + |c| c.is_alphanumeric().then_some(c) + ).collect()) + ); + + let config = EmulatorConfig { + dimensions, fullscreen, zoom, palette, show_cursor, + decode_stdin, encode_stdout, trust_files, + symbols_path, name, identifier, title, icon, + }; + + match Phosphor::new() { + Ok(phosphor) => match mode { + Mode::Dynamic => { + info!("Starting graphical emulator (hidden)"); + let mut emulator = GraphicalEmulator::new(config, debug); + emulator.load_program(&bytecode); + emulator.run(phosphor, false); + } + Mode::Graphical => { + info!("Starting graphical emulator"); + let mut emulator = GraphicalEmulator::new(config, debug); + emulator.load_program(&bytecode); + emulator.run(phosphor, true); + } + Mode::Headless => { + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + } + Err(err) => match mode { + Mode::Dynamic => { + eprintln!("EventLoopError: {err:?}"); + info!("Could not start graphical event loop"); + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + Mode::Graphical => { + eprintln!("EventLoopError: {err:?}"); + fatal!("Could not start graphical event loop"); + } + Mode::Headless => { + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + } + } +} + + +fn print_help() { + eprintln!("\ +Usage: br [source] + br asm [source] [destination] + +Emulator and assembler for the Bedrock computer system. + +To access the assembler, run `br asm`. To learn how to use the +assembler, run `br asm --help`. + +Usage: + To load a Bedrock program from a file, run `br <path>`, where + <path> is the path to an assembled Bedrock program. + + To load a Bedrock program from piped input, run `<command> | br`, + where <command> is a command that generates Bedrock bytecode. + + To assemble a Bedrock program from a source file and write to an + output file, run `br asm <source> <output>`, where <source> is the + path of the source file and <output> is the path to write to. + + To assemble and run a Bedrock program without saving to a file, + run `br asm <source> | br`, where <source> is the path of the + source file. + +Environment variables: + BEDROCK_PATH + A list of colon-separated paths that will be searched to find + a Bedrock program when the program doesn't exist in the current + directory. This allows the user to run frequently-used programs + from any directory. + BEDROCK_LIBS + A list of colon-separated paths that will be searched to find + Bedrock source code files to use as libraries when assembling a + Bedrock program. If a library file resolves an unresolved symbol + in the program being assembled, the library file will be merged + into the program. + +Arguments: + [program] Path to a Bedrock program to run + +Switches: + --fullscreen (-f) Start the program in fullscreen mode (toggle with F11) + --size=<dim> (-s) Set the initial window size in the format <width>x<height> + --zoom=<scale> (-z) Set the pixel size for the screen (change with F5/F6) + --show-cursor (-c) Show the operating system cursor over the window + --palette=<pal> Set a debug colour palette in the format <rgb>,... (toggle with F2) + --debug, (-d) Show debug information while the program is running + --decode-stdin (-i) Decode transmissions on standard input from text lines. + --encode-stdout (-o) Encode transmissions on standard output as text lines. + --trust-files Give the program unrestricted access to the file system. + --help (-h) Print this help information + --verbose, (-v) Print additional information + --version Print the program version and exit +"); +} |