use std::ffi::OsString;
use std::path::PathBuf;
#[derive(Debug, PartialEq)]
pub enum Action {
Help,
Version,
Check,
Run(Options),
}
#[derive(Debug, PartialEq)]
pub struct Options {
pub socket: Option<PathBuf>,
pub directory: Option<PathBuf>,
pub font: Option<PathBuf>,
pub font_size: f32,
pub width: u32,
pub height: u32,
pub scrollback: usize,
pub hold: bool,
pub command: Command,
}
#[derive(Debug, PartialEq)]
pub enum Command {
Shell,
Script(OsString),
Argv(Vec<OsString>),
}
pub fn parse(args: impl IntoIterator<Item = OsString>) -> Result<Action, String> {
let mut args = args.into_iter();
let mut options = Options {
socket: None,
directory: None,
font: None,
font_size: 16.0,
width: 960,
height: 640,
scrollback: 1000,
hold: false,
command: Command::Shell,
};
while let Some(arg) = args.next() {
let name = arg.to_str().ok_or("option name must be UTF-8")?;
match name {
"-h" | "--help" => return Ok(Action::Help),
"-V" | "--version" => return Ok(Action::Version),
"--check" => return Ok(Action::Check),
"--hold" => options.hold = true,
"--socket" => options.socket = Some(value(&mut args, name)?.into()),
"--working-directory" => options.directory = Some(value(&mut args, name)?.into()),
"--font-file" => options.font = Some(value(&mut args, name)?.into()),
"--font-size" => {
options.font_size = number(&mut args, name)?;
if !options.font_size.is_finite() || !(8.0..=48.0).contains(&options.font_size) {
return Err("--font-size must be between 8 and 48".into());
}
}
"--width" => options.width = number(&mut args, name)?,
"--height" => options.height = number(&mut args, name)?,
"--scrollback" => {
options.scrollback = number(&mut args, name)?;
if options.scrollback > 5000 {
return Err("--scrollback must be at most 5000 lines".into());
}
}
"-e" | "--command" => {
options.command = Command::Script(value(&mut args, name)?);
if args.next().is_some() {
return Err(
"-e accepts one shell command string; use -- for an argument vector".into(),
);
}
break;
}
"--" => {
let command: Vec<_> = args.collect();
if command.first().is_none_or(|s| s.is_empty()) {
return Err("-- requires a command".into());
}
options.command = Command::Argv(command);
break;
}
_ => return Err(format!("unknown Aen option: {name}; see lios --help")),
}
}
aen_window::frame_bytes(options.width, options.height).map_err(|e| e.to_string())?;
Ok(Action::Run(options))
}
fn value(args: &mut impl Iterator<Item = OsString>, name: &str) -> Result<OsString, String> {
args.next()
.filter(|v| !v.is_empty())
.ok_or_else(|| format!("{name} requires a value"))
}
fn number<T: std::str::FromStr>(
args: &mut impl Iterator<Item = OsString>,
name: &str,
) -> Result<T, String> {
value(args, name)?
.to_str()
.and_then(|s| s.parse().ok())
.ok_or_else(|| format!("invalid {name} value"))
}
pub const HELP: &str = "Lios Terminal — native Aen frontend\n\nUsage: lios [options] [-- command arguments...]\n -e, --command STRING Run a shell command string\n --working-directory PATH Set the child working directory\n --socket PATH Select the native application endpoint\n --width N --height N Preferred window size (compositor decides placement)\n --font-file PATH Monospace TTF/OTF font\n --font-size N Font height, 8–48 pixels (default 16)\n --scrollback N History lines, 0–5000 (default 1000)\n --hold Keep the window after the child exits\n --check Inspect native build/font/endpoint without connecting\n -h, --help Show help\n -V, --version Show version and frontend\n\nShift+PageUp/PageDown or the wheel scroll history. Drag selects text.\nCtrl+Shift+C/V uses the compositor-owned native text clipboard (64 KiB maximum).\n";
#[cfg(test)]
mod tests {
use super::*;
fn args(values: &[&str]) -> Vec<OsString> {
values.iter().map(OsString::from).collect()
}
#[test]
fn argv_is_preserved_without_shell_interpretation() {
let Action::Run(options) = parse(args(&["--", "printf", "%s", "a b;$HOME"])).unwrap()
else {
panic!()
};
assert_eq!(
options.command,
Command::Argv(args(&["printf", "%s", "a b;$HOME"]))
);
}
#[test]
fn rejects_invalid_and_unbounded_options() {
for values in [
&["--width", "0"][..],
&["--font-size", "NaN"],
&["--scrollback", "5001"],
&["--"],
&["-e", "echo", "extra"],
] {
assert!(parse(args(values)).is_err(), "{values:?}");
}
}
}