summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Bridle <ben@derelict.engineering>2025-02-03 20:12:02 +1300
committerBen Bridle <ben@derelict.engineering>2025-02-03 20:12:02 +1300
commit07ae3438917fd854a46924a410f6890cd0651f1b (patch)
tree00ba5b82a77f4f379fd55c6c6e329ee0644e50e2
parenta2408cd14317e1c0969953f8745034d78b2b9bcf (diff)
downloadbedrock-pc-07ae3438917fd854a46924a410f6890cd0651f1b.zip
Use switchboard crate for parsing command line arguments
-rw-r--r--Cargo.lock17
-rw-r--r--Cargo.toml2
-rw-r--r--src/bin/br.rs416
3 files changed, 210 insertions, 225 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 1409c02..81b9a4b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -99,8 +99,8 @@ dependencies = [
"geometry",
"log 1.1.1",
"phosphor",
+ "switchboard",
"windows",
- "xflags",
]
[[package]]
@@ -931,6 +931,12 @@ dependencies = [
]
[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1191,6 +1197,15 @@ dependencies = [
]
[[package]]
+name = "switchboard"
+version = "1.0.0"
+source = "git+git://benbridle.com/switchboard?tag=v1.0.0#ea70fa89659e5cf1a9d4ca6ea31fb67f7a2cc633"
+dependencies = [
+ "log 1.1.1",
+ "paste",
+]
+
+[[package]]
name = "syn"
version = "2.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index eb46468..448a71f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,9 +11,9 @@ bedrock-core = { git = "git://benbridle.com/bedrock-core", tag = "v5.0.0" }
phosphor = { git = "git://benbridle.com/phosphor", tag = "v3.2.2" }
geometry = { git = "git://benbridle.com/geometry", tag = "v1.0.0" }
log = { git = "git://benbridle.com/log", tag = "v1.1.1" }
+switchboard = { git = "git://benbridle.com/switchboard", tag = "v1.0.0" }
chrono = { version = "0.4.38" }
-xflags = "0.4.0-pre.1"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.58.0", features = ["Win32_Storage_FileSystem"] }
diff --git a/src/bin/br.rs b/src/bin/br.rs
index 8218286..d7bde60 100644
--- a/src/bin/br.rs
+++ b/src/bin/br.rs
@@ -2,33 +2,133 @@ use bedrock_asm::*;
use bedrock_pc::*;
use phosphor::*;
+use log::{info, fatal};
+use switchboard::{Switchboard, SwitchQuery};
+
+use std::cmp::max;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::exit;
-use log::{info, fatal};
+
+fn print_help() -> ! {
+ println!("\
+Usage: br [source]
+ br asm [source] [destination] [extension]
+
+Integrated Bedrock assembler and emulator.
+
+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 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.
+
+Arguments:
+ [program] Path to a Bedrock program to run
+
+Switches:
+ --verbose, (-v) Print additional debug information
+ --version Print the assembler version and exit
+ --help (-h) Prints help
+ --debug, (-d) Show debug information while the program is running
+ --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)
+ --palette=<pal> Set a debug colour palette in the format <rgb>,... (toggle with F2)
+ --show-cursor (-c) Show the operating system cursor over the window
+ --decode-stdin (-i) Decode standard input
+ --encode-stdout (-o) Encode standard output
+
+Arguments (asm mode):
+ [source] Bedrock source code file to assemble.
+ [destination] Destination path for assembler output.
+ [extension] File extension to identify source files.
+
+Switches (asm mode):
+ --no-libs Don't include libraries or resolve references.
+ --no-project-libs Don't include project libraries
+ --no-env-libs Don't include environment libraries.
+ --tree Show the resolved source file heirarchy
+ --check Assemble the program without saving any output
+ --resolve Only return resolved source code.
+ --symbols Generate debug symbols with file extension '.br.sym'
+");
+ std::process::exit(0);
+}
+
+
+fn print_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);
+}
+
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);
+ let mut args = Switchboard::from_env();
+ if args.named("help").short('h').as_bool() {
+ print_help();
+ }
+ if args.named("version").as_bool() {
+ print_version();
}
- if args.verbose {
+ if args.named("verbose").short('v').as_bool() {
log::set_log_level(log::LogLevel::Info);
}
- match args.subcommand {
- ArgumentsCmd::Run(run) => main_run(run),
- ArgumentsCmd::Asm(asm) => main_asm(asm),
+ match args.peek() {
+ Some("run") => { args.pop(); main_run(args) },
+ Some("asm") => { args.pop(); main_asm(args) },
+ _ => main_run(args),
}
}
-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);
+fn main_run(mut args: Switchboard) {
+ let source = args.positional("source").as_path_opt();
+ let debug = args.named("debug").short('d').as_bool();
+ let fullscreen = args.named("fullscreen").short('f').as_bool();
+ let scale = max(1, args.named("zoom").short('z').default("1").quick("3").as_u32());
+ let dimensions = match args.named("size").short('s').as_string_opt() {
+ Some(string) => parse_dimensions(&string).unwrap_or_else(||
+ fatal!("Invalid dimensions string {string:?}")),
+ None => DEFAULT_SCREEN_SIZE / (scale as u16),
+ };
+ let debug_palette = match args.named("palette").as_string_opt() {
+ Some(string) => match parse_palette(&string) {
+ Some(palette) => Some(palette),
+ None => fatal!("Invalid palette string {string:?}"),
+ },
+ None => None,
+ };
+ let show_cursor = args.named("show-cursor").short('c').as_bool();
+ let decode_stdin = args.named("decode-stdin").short('i').as_bool();
+ let encode_stdout = args.named("encode-stdout").short('o').as_bool();
+
+ let Bytecode { bytes: bytecode, path } = load_bytecode(source.as_ref());
let symbols_path = path.as_ref().map(|p| {
let mut path = p.to_path_buf();
path.set_extension("br.sym");
@@ -40,29 +140,28 @@ fn main_run(args: Run) {
info!("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,
+ let config = EmulatorConfig {
+ dimensions,
+ fullscreen,
+ scale,
+ debug_palette,
+ show_cursor,
initial_transmission: None,
- decode_stdin: args.decode_stdin,
- encode_stdout: args.encode_stdout,
+ decode_stdin,
+ encode_stdout,
symbols_path,
};
let phosphor = Phosphor::new();
- if phosphor.is_ok() && args.dimensions().is_some() {
+ if phosphor.is_ok() && dimensions.area_usize() != 0 {
info!("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);
+ let mut graphical = GraphicalEmulator::new(&config, debug);
graphical.load_program(&bytecode);
if let EmulatorSignal::Promote = graphical.run() {
let program_name = match &metadata {
@@ -88,16 +187,16 @@ fn main_run(args: Run) {
}
} else {
info!("Starting headless emulator");
- let mut headless = HeadlessEmulator::new(&config, args.debug);
+ let mut headless = HeadlessEmulator::new(&config, debug);
headless.load_program(&bytecode);
- headless.run(args.debug);
+ headless.run(debug);
};
std::process::exit(0);
}
-fn load_bytecode(path: Option<&Path>) -> Bytecode {
- // TODO: Etch file location into bytecode.
+fn load_bytecode(path: Option<&PathBuf>) -> Bytecode {
+ // TODO: Etch file location into bytecode as per metadata.
if let Some(path) = path {
if let Ok(bytecode) = load_bytecode_from_file(path) {
let length = bytecode.bytes.len();
@@ -184,21 +283,33 @@ impl Bytecode {
}
-fn main_asm(args: Asm) {
+fn main_asm(mut args: Switchboard) {
+ let source = args.positional("source").as_path_opt();
+ let destination = args.positional("source").as_path_opt();
+ let extension = args.positional("extension").default("brc").as_string();
+
+ let no_libs = args.named("no-libs").as_bool();
+ let no_project_libs = args.named("no-project-libs").as_bool();
+ let no_environment_libs = args.named("no-env-libs").as_bool();
+
+ let print_tree = args.named("tree").as_bool();
+ let dry_run = args.named("check").as_bool();
+ let only_resolve = args.named("resolve").as_bool();
+ let export_symbols = args.named("symbols").as_bool();
+
// -----------------------------------------------------------------------
// RESOLVE syntactic symbols
- let ext = args.ext.unwrap_or(String::from("brc"));
- let source_path = args.source.clone().map(|p| {
+ let source_path = source.clone().map(|p| {
p.canonicalize().unwrap_or(p)
});
let mut resolver = if let Some(path) = &source_path {
- match SourceUnit::from_path(&path, &ext) {
+ match SourceUnit::from_path(&path, &extension) {
Ok(source_unit) => SymbolResolver::from_source_unit(source_unit),
Err(err) => {
match err {
ParseError::InvalidExtension => fatal!(
- "File {path:?} has invalid extension, must be '.{ext}'"),
+ "File {path:?} has invalid extension, must be '.{extension}'"),
ParseError::NotFound => fatal!(
"File {path:?} was not found"),
ParseError::InvalidUtf8 => fatal!(
@@ -224,14 +335,14 @@ fn main_asm(args: Asm) {
};
// 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);
+ if !no_libs && !no_project_libs {
+ let project_library = gather_project_libraries(path, &extension);
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) {
+ if !no_libs && !no_environment_libs {
+ for env_library in gather_environment_libraries(&extension) {
resolver.add_library_units(env_library);
}
}
@@ -239,7 +350,7 @@ fn main_asm(args: Asm) {
// -----------------------------------------------------------------------
// PRINT information, generate merged source code
- if args.tree {
+ if print_tree {
print_source_tree(&resolver);
}
if print_resolver_errors(&resolver) {
@@ -252,8 +363,8 @@ fn main_asm(args: Asm) {
std::process::exit(1);
},
};
- if args.resolve && !args.check {
- write_bytes_and_exit(merged_source.as_bytes(), args.output.as_ref());
+ if only_resolve && !dry_run {
+ write_bytes_and_exit(merged_source.as_bytes(), destination.as_ref());
}
// -----------------------------------------------------------------------
@@ -268,8 +379,8 @@ fn main_asm(args: Asm) {
// GENERATE symbols file and bytecode
let bytecode = generate_bytecode(&mut semantic_tokens);
- if args.symbols && !args.check {
- if let Some(path) = &args.output {
+ if export_symbols && !dry_run {
+ if let Some(path) = &destination {
let mut symbols_path = path.to_path_buf();
symbols_path.set_extension("br.sym");
let symbols = generate_symbols_file(&semantic_tokens);
@@ -286,8 +397,8 @@ fn main_asm(args: Asm) {
let percentage = (length as f32 / 65536.0 * 100.0).round() as u16;
info!("Assembled program in {length} bytes ({percentage}% of maximum)");
- if !args.check {
- write_bytes_and_exit(&bytecode, args.output.as_ref());
+ if !dry_run {
+ write_bytes_and_exit(&bytecode, destination.as_ref());
}
}
@@ -305,187 +416,46 @@ fn write_bytes_and_exit<P: AsRef<Path>>(bytes: &[u8], path: Option<&P>) -> ! {
}
-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 -c, --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
- }
+fn parse_dimensions(string: &str) -> Option<ScreenDimensions> {
+ if string.trim().to_lowercase() == "none" {
+ return Some(ScreenDimensions::ZERO);
}
+ let (w_str, h_str) = string.trim().split_once('x')?;
+ Some( ScreenDimensions {
+ width: w_str.parse().ok()?,
+ height: h_str.parse().ok()?,
+ } )
}
-
-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),
+fn parse_palette(string: &str) -> Option<[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 }
}
}
-
- 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")
+ 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,
+ })
}