//! xfig's command line (plan S3): the options of `xfig.man.in` for RustFig. //! //! Options with an equivalent become resource values that override the //! preferences for one run (never written to the settings file). X toolkit //! options are accepted; the ones without meaning here are ignored with a //! note. As in Xt, an option may be abbreviated to a prefix: the exact name //! wins, otherwise the first option of the table that starts with the //! argument (application options before the toolkit's). Options that take a //! value consume the next argument, whatever it looks like. // // Ported from xfig src/main.c (`options`, `help_list`, argument handling) // and doc/xfig.man.in (xfig licence, see LICENSE-xfig). use crate::prefs::{resource, Prefs, NOT_APPLICABLE}; /// What an option does. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Action { /// Sets resource `.0` to the fixed value `.1`. Flag(&'static str, &'static str), /// Sets resource `.0` to the next argument. Arg(&'static str), /// X toolkit option without an argument, ignored. IgnoreFlag, /// X toolkit option with an argument, ignored. IgnoreArg, } use Action::{Arg, Flag, IgnoreArg, IgnoreFlag}; /// xfig's option table (`main.c`), in its order, then the X toolkit's. #[rustfmt::skip] pub const OPTIONS: &[(&str, Action)] = &[ ("-visual", Arg("visual")), ("-depth", Arg("depth")), ("-allownegcoords", Flag("allownegcoords", "true")), ("-autorefresh", Flag("autorefresh", "true")), ("-balloon_delay", Arg("balloon_delay")), ("-browser", Arg("browser")), ("-pdf_viewer", Arg("pdf_viewer")), ("-buttonFont", Arg("buttonFont")), ("-but_per_row", Arg("but_per_row")), ("-cbg", Arg("canvasbackground")), ("-center", Flag("flushleft", "false")), ("-centimeters", Flag("inches", "false")), ("-cfg", Arg("canvasforeground")), ("-correct_font_size", Flag("correctfontsize", "true")), ("-crosshair", Flag("crosshair", "true")), ("-debug", Flag("debug", "true")), ("-dontallownegcoords", Flag("allownegcoords", "false")), ("-dontshowaxislines", Flag("showaxislines", "false")), ("-dontshowballoons", Flag("showballoons", "false")), ("-dontshowlengths", Flag("showlengths", "false")), ("-dontshownums", Flag("shownums", "false")), ("-dontshowpageborder", Flag("showpageborder", "false")), ("-dontswitchcmap", Flag("dontswitchcmap", "true")), ("-encoding", Arg("encoding")), ("-exportLanguage", Arg("exportLanguage")), ("-export_margin", Arg("export_margin")), ("-flipvisualhints", Flag("flipvisualhints", "true")), ("-noflipvisualhints", Flag("flipvisualhints", "false")), ("-flushleft", Flag("flushleft", "true")), ("-freehand_resolution", Arg("freehand_resolution")), ("-ghostscript", Arg("ghostscript")), ("-grid_color", Arg("grid_color")), ("-grid_unit", Arg("grid_unit")), ("-gslib", Arg("gslib")), ("-hiddentext", Flag("hiddentext", "true")), ("-dontshowdepthmanager", Flag("showdepthmanager", "false")), ("-iconGeometry", Arg("iconGeometry")), ("-icon_view", Flag("icon_view", "true")), ("-image_editor", Arg("image_editor")), ("-imperial", Flag("inches", "true")), ("-inches", Flag("inches", "true")), ("-installowncmap", Flag("installowncmap", "true")), ("-internalBW", Arg("internalborderwidth")), ("-jpeg_quality", Arg("jpeg_quality")), ("-keyFile", Arg("keyFile")), ("-Landscape", Flag("landscape", "true")), ("-landscape", Flag("landscape", "true")), ("-latexfonts", Flag("latexfonts", "true")), ("-left", Flag("justify", "false")), ("-library_dir", Arg("library_dir")), ("-library_icon_size", Arg("library_icon_size")), ("-list_view", Flag("icon_view", "false")), ("-magnification", Arg("magnification")), ("-max_image_colors", Arg("max_image_colors")), ("-metric", Flag("inches", "false")), ("-monochrome", Flag("monochrome", "true")), ("-multiple", Flag("multiple", "true")), ("-nooverlap", Flag("overlap", "false")), ("-normalFont", Arg("normalFont")), ("-nosplash", Flag("splash", "false")), ("-notrack", Flag("trackCursor", "false")), ("-nowrite_bak", Flag("write_bak", "false")), ("-overlap", Flag("overlap", "true")), ("-pageborder", Arg("pageborder")), ("-paper_size", Arg("paper_size")), ("-pheight", Arg("pheight")), ("-Portrait", Flag("landscape", "false")), ("-portrait", Flag("landscape", "false")), ("-pwidth", Arg("pwidth")), ("-right", Flag("justify", "true")), ("-rigidtext", Flag("rigidtext", "true")), ("-rulerthick", Arg("rulerthick")), ("-scale_factor", Arg("scale_factor")), ("-showallbuttons", Flag("showallbuttons", "true")), ("-showballoons", Flag("showballoons", "true")), ("-showdepthmanager", Flag("showdepthmanager", "true")), ("-showlengths", Flag("showlengths", "true")), ("-shownums", Flag("shownums", "true")), ("-showpageborder", Flag("showpageborder", "true")), ("-showaxislines", Flag("showaxislines", "true")), ("-single", Flag("multiple", "false")), ("-smooth_factor", Arg("smooth_factor")), ("-smallicons", Flag("smallicons", "true")), ("-specialtext", Flag("specialtext", "true")), ("-spellcheckcommand", Arg("spellcheckcommand")), ("-spinner_delay", Arg("spinner_delay")), ("-spinner_rate", Arg("spinner_rate")), ("-splash", Flag("splash", "true")), ("-startfillstyle", Arg("startfillstyle")), ("-startarrowtype", Arg("startarrowtype")), ("-startarrowthick", Arg("startarrowthick")), ("-startarrowwidth", Arg("startarrowwidth")), ("-startarrowlength", Arg("startarrowlength")), ("-startFontSize", Arg("startfontsize")), ("-startfontsize", Arg("startfontsize")), ("-startgridmode", Arg("startgridmode")), ("-startgridtype", Arg("startgridtype")), ("-startlatexFont", Arg("startlatexFont")), ("-startlinewidth", Arg("startlinewidth")), ("-startposnmode", Arg("startposnmode")), ("-startpsFont", Arg("startpsFont")), ("-starttextstep", Arg("starttextstep")), ("-tablet", Flag("tablet", "true")), ("-track", Flag("trackCursor", "true")), ("-transparent_color", Arg("transparent_color")), ("-userscale", Arg("userscale")), ("-write_v40", Flag("write_v40", "true")), ("-write_bak", Flag("write_bak", "true")), ("-userunit", Arg("userunit")), ("-axislines", Arg("axislines")), ("-zoom", Arg("zoom")), ("-international", Flag("international", "true")), ("-nointernational", Flag("international", "false")), ("-inputStyle", Arg("inputStyle")), // The X toolkit's own options (Xt appends them after the application's). ("-geometry", Arg("geometry")), ("-display", IgnoreArg), ("-iconic", IgnoreFlag), ("-name", IgnoreArg), ("-title", IgnoreArg), ("-background", IgnoreArg), ("-bg", IgnoreArg), ("-foreground", IgnoreArg), ("-fg", IgnoreArg), ("-bordercolor", IgnoreArg), ("-bd", IgnoreArg), ("-borderwidth", IgnoreArg), ("-bw", IgnoreArg), ("-font", IgnoreArg), ("-fn", IgnoreArg), ("-reverse", IgnoreFlag), ("-rv", IgnoreFlag), ("+rv", IgnoreFlag), ("-synchronous", IgnoreFlag), ("+synchronous", IgnoreFlag), ("-selectionTimeout", IgnoreArg), ("-xnllanguage", IgnoreArg), ("-xtsessionID", IgnoreArg), ("-xrm", IgnoreArg), ]; /// The result of reading the command line. #[derive(Debug, Clone, Default, PartialEq)] pub struct Parsed { /// The Fig file to open. pub file: Option, /// `--control PORT`. pub control: Option, /// `--background`. pub background: bool, /// `-help`, `-h`, `--help`: print the usage and quit. pub help: bool, /// `-v`, `-V`, `--version`: print the version and quit. pub version: bool, /// `-update FILE...`: rewrite the files in the current format and quit. pub update: Option>, /// Resource values for this run, in order (a later one wins). pub overrides: Vec<(String, String)>, /// Messages for stderr (ignored options, bad values). pub notes: Vec, } /// The option `arg` names: an exact match, else the first that starts with it. pub fn lookup(arg: &str) -> Option<&'static (&'static str, Action)> { if arg.len() < 2 { return None; } OPTIONS .iter() .find(|(n, _)| *n == arg) .or_else(|| OPTIONS.iter().find(|(n, _)| n.starts_with(arg))) } /// Read the arguments (without the program name). An unknown option, a /// missing value or a second file name is an error, as in xfig. pub fn parse(args: I) -> Result where I: IntoIterator, S: AsRef, { let args: Vec = args.into_iter().map(|a| a.as_ref().to_string()).collect(); let mut p = Parsed::default(); if args.first().is_some_and(|a| a.eq_ignore_ascii_case("-update")) { let (opts, files): (Vec<_>, Vec<_>) = args.iter().skip(1).partition(|a| a.starts_with('-')); for a in opts { p.notes.push(format!("-update: option {a} ignored")); } p.update = Some(files.into_iter().cloned().collect()); return Ok(p); } let mut it = args.iter(); while let Some(a) = it.next() { match a.as_str() { "-help" | "-h" | "--help" => p.help = true, "-V" | "--version" => p.version = true, "--background" => p.background = true, "--control" => { let v = it.next().ok_or("rustfig: --control needs a port")?; p.control = Some(v.parse().map_err(|_| format!("rustfig: bad control port {v:?}"))?); } _ if a.eq_ignore_ascii_case("-v") => p.version = true, _ if (a.starts_with('-') || a.starts_with('+')) && a.len() > 1 => { let Some((name, action)) = lookup(a) else { return Err(format!("xfig: unknown option or missing parameter: {a}")); }; let missing = || format!("xfig: unknown option or missing parameter: {a}"); match *action { Flag(r, v) => p.set(r, v.to_string(), name), Arg(r) => { let v = it.next().cloned().ok_or_else(missing)?; p.set(r, v, name); } IgnoreFlag => p.notes.push(format!("{name}: X toolkit option, ignored")), IgnoreArg => { let v = it.next().cloned().ok_or_else(missing)?; if *name == "-xrm" { p.xrm(&v); } else { p.notes.push(format!("{name} {v}: X toolkit option, ignored")); } } } } _ => { if let Some(first) = &p.file { return Err(format!("xfig: too many parameters: {a} (after {first})")); } p.file = Some(a.clone()); } } } Ok(p) } impl Parsed { /// Record resource `name` = `value`, or say why it has no effect. fn set(&mut self, name: &str, value: String, option: &str) { if NOT_APPLICABLE.iter().any(|n| n.eq_ignore_ascii_case(name)) { self.notes.push(format!("{option}: no meaning in RustFig, ignored")); return; } if resource(name).is_none() { self.notes.push(format!("{option}: not supported yet, ignored")); return; } // Check the value's type the way the preferences do. if let Err(e) = Prefs::default().set(name, toml::Value::String(value.clone())) { self.notes.push(format!("{option}: {e}, ignored")); return; } self.overrides.push((name.to_string(), value)); } /// `-xrm "Fig.name: value"`. fn xrm(&mut self, spec: &str) { match parse_resources(spec).pop() { Some((n, v)) => self.set(&n, v, "-xrm"), None => self.notes.push(format!("-xrm {spec}: not a Fig resource, ignored")), } } /// The overrides as a preferences table (`[xfig]` values, typed). pub fn override_table(&self) -> toml::Table { resource_table(&self.overrides) } } /// `(resource, text)` pairs as typed `[xfig]` values; pairs that don't fit /// their resource are left out. pub fn resource_table(pairs: &[(String, String)]) -> toml::Table { let mut p = Prefs::default(); for (n, v) in pairs { let _ = p.set(n, toml::Value::String(v.clone())); // a wrong type: the pair is dropped } p.xfig } /// The `Fig.name: value` (also `xfig*name:`) lines of an X resource file /// (`~/.Xdefaults` style) as (resource, value); comments (`!`) and other /// applications' lines are skipped. pub fn parse_resources(text: &str) -> Vec<(String, String)> { let mut out = Vec::new(); for line in text.lines() { let line = line.trim(); if line.starts_with('!') || line.starts_with('#') { continue; } let Some((key, value)) = line.split_once(':') else { continue; }; let key = key.trim(); let name = ["Fig.", "Fig*", "xfig.", "xfig*", "*"] .iter() .find_map(|pre| key.strip_prefix(pre)); if let Some(name) = name { if !name.contains(['.', '*']) && !name.is_empty() { out.push((name.to_string(), value.trim().to_string())); } } } out } /// The usage text (`-help`), as xfig prints it. pub fn usage() -> String { let mut s = String::from("Usage:\nxfig [-h[elp]] [-v[ersion]]\n "); let mut col = 5; for (n, a) in OPTIONS { let item = match a { Flag(..) | IgnoreFlag => format!("[{n}] "), _ => format!("[{n} ] "), }; if col + item.len() > 85 { s.push_str("\n "); col = 5; } col += item.len(); s.push_str(&item); } s.push_str( "[-update file1 file2 ...] [--control PORT] [--background] [file]\n Note: all options may be abbreviated to minimum unique string.\n", ); s } #[cfg(test)] mod tests { use super::*; fn p(args: &[&str]) -> Parsed { parse(args.iter().copied()).unwrap() } fn has(p: &Parsed, n: &str, v: &str) -> bool { p.overrides.iter().any(|(a, b)| a == n && b == v) } #[test] fn every_option_is_accepted() { // Every table entry parses (a value is supplied where needed) and // either sets a resource or says why not; none is an error. for (name, action) in OPTIONS { let mut args = vec![name.to_string()]; match action { Arg(r) => args.push(match resource(r).map(|r| r.kind) { Some(crate::prefs::Kind::Str) | None => "x".into(), _ => "3".into(), }), IgnoreArg => args.push("v".into()), _ => {} } let r = parse(&args).unwrap_or_else(|e| panic!("{name}: {e}")); assert!( !r.overrides.is_empty() || !r.notes.is_empty(), "{name} has no effect and no note" ); } } #[test] fn options_with_equivalents_set_resources() { assert!(has(&p(&["-metric"]), "inches", "false")); assert!(has(&p(&["-inches"]), "inches", "true")); assert!(has(&p(&["-imperial"]), "inches", "true")); assert!(has(&p(&["-centimeters"]), "inches", "false")); assert!(has(&p(&["-specialtext"]), "specialtext", "true")); assert!(has(&p(&["-latexfonts"]), "latexfonts", "true")); assert!(has(&p(&["-startfontsize", "16"]), "startfontsize", "16")); assert!(has(&p(&["-startgridmode", "2"]), "startgridmode", "2")); assert!(has(&p(&["-startposnmode", "0"]), "startposnmode", "0")); assert!(has(&p(&["-zoom", "1.5"]), "zoom", "1.5")); assert!(has(&p(&["-exportLanguage", "pdf"]), "exportLanguage", "pdf")); assert!(has(&p(&["-startarrowtype", "2"]), "startarrowtype", "2")); assert!(has(&p(&["-portrait"]), "landscape", "false")); assert!(has(&p(&["-Landscape"]), "landscape", "true")); assert!(has(&p(&["-right"]), "justify", "true")); assert!(has(&p(&["-flushleft"]), "flushleft", "true")); assert!(has(&p(&["-correct_font_size"]), "correctfontsize", "true")); assert!(has(&p(&["-paper_size", "A4"]), "paper_size", "A4")); assert!(has(&p(&["-nowrite_bak"]), "write_bak", "false")); } #[test] fn values_are_consumed_even_if_they_look_like_options() { let r = p(&["-startpsFont", "-metric", "a.fig"]); assert!(has(&r, "startpsFont", "-metric")); assert!(!has(&r, "inches", "false")); assert_eq!(r.file.as_deref(), Some("a.fig")); } #[test] fn abbreviations_follow_xt() { assert_eq!(lookup("-me").map(|o| o.0), Some("-metric")); assert_eq!(lookup("-spec").map(|o| o.0), Some("-specialtext")); assert_eq!(lookup("-startfo").map(|o| o.0), Some("-startfontsize")); assert_eq!(lookup("-lan").map(|o| o.0), Some("-landscape")); assert_eq!( lookup("-center").map(|o| o.0), Some("-center"), "exact wins over -centimeters" ); assert_eq!(lookup("-ce").map(|o| o.0), Some("-center"), "first of the table"); assert!(has(&p(&["-zo", "2"]), "zoom", "2")); assert!(has(&p(&["-startpo", "3"]), "startposnmode", "3")); assert!(lookup("-nosuchthing").is_none()); assert!(lookup("-").is_none()); } #[test] fn toolkit_options_are_accepted_and_noted() { let r = p(&[ "-display", ":0", "-iconic", "-bg", "red", "+rv", "-geometry", "800x600+10+10", "x.fig", ]); assert_eq!(r.file.as_deref(), Some("x.fig")); assert!(has(&r, "geometry", "800x600+10+10")); assert_eq!(r.notes.len(), 4, "{:?}", r.notes); let r = p(&["-xrm", "Fig.startfontsize: 20"]); assert!(has(&r, "startfontsize", "20")); } #[test] fn options_without_meaning_are_noted_not_applied() { let r = p(&["-visual", "TrueColor", "-monochrome", "-debug", "-nosplash"]); assert!(r.overrides.is_empty()); assert_eq!(r.notes.len(), 4); } #[test] fn errors_and_special_forms() { assert!(parse(["-bogus"]).is_err()); assert!(parse(["-zoom"]).is_err(), "missing value"); assert!(parse(["a.fig", "b.fig"]).is_err()); assert!(parse(["--control", "x"]).is_err()); assert!(p(&["-help"]).help && p(&["-h"]).help && p(&["-v"]).version && p(&["-V"]).version); let r = p(&["--control", "7986", "--background", "-metric", "f.fig"]); assert_eq!((r.control, r.background), (Some(7986), true)); let r = p(&["-update", "a.fig", "b.fig"]); assert_eq!(r.update, Some(vec!["a.fig".into(), "b.fig".into()])); // A bad value is noted and dropped, not an error. let r = p(&["-zoom", "big"]); assert!(r.overrides.is_empty() && r.notes.len() == 1); assert_eq!( p(&["-metric"]).override_table().get("inches"), Some(&toml::Value::Boolean(false)) ); assert!(usage().contains("[-metric]")); // Hostile arguments don't panic. for a in ["", "-", "+", "--", "é", "-é", "\u{0}"] { let _ = parse([a]); } } #[test] fn resource_files_are_read() { let r = parse_resources("! c\nFig.inches: false\nxfig*startfontsize: 14\nOther.x: 1\nFig.a.b: 2\n*zoom: 2\n"); assert_eq!(r.len(), 3); assert_eq!(r[0], ("inches".into(), "false".into())); } } .