Files
cadcraft/crates/engine/src/cmd/options.rs
T
fde62a39ab MEASUREINIT: choose the units of new drawings, kept in the profile (#1792)
* MEASUREINIT: choose the units of new drawings, kept in the profile

NEW, QNEW and the first drawing at startup were always imperial unless a
template or {"metric": true} was given, and nothing kept a metric choice
across restarts. MEASUREINIT (0 imperial, the default; 1 metric) is now a
profile variable: NEW without `metric` or a template follows it, the app's
startup drawing follows it, and Options > Files shows it as "Units of new
drawings". QNEWTEMPLATE and an explicit `metric` still win.

Part of #1182.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxn7PD9WzP4ET2LU7BRLDK

* contributors: add Richard Sauer (aussieboer61)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxn7PD9WzP4ET2LU7BRLDK

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:54:51 -04:00

195 lines
6.2 KiB
Rust

//! OPTIONS: the user-profile settings (the system variables a user keeps across sessions and
//! drawings, as AutoCAD keeps them in the user profile rather than in the drawing).
//!
//! The JSON form reads and changes them without a dialog: `{}` returns every profile variable by
//! name, `{"PICKBOX": 5, "OSMODE": 7}` sets those (all or nothing), `{"reset": true}` restores the
//! defaults first. The desktop app opens its Options dialog when OPTIONS is typed or chosen from
//! the menu, and saves [`profile`] between runs ([`restore`] reads it back).
use serde_json::{Map, Value, json};
use super::*;
use crate::{Result, Session, Settings};
/// The profile variables, in the order [`profile`] lists them.
pub const PROFILE_VARS: &[&str] = &[
"OSMODE",
"AUTOSNAP",
"POLARMODE",
"POLARANG",
"POLARADDANG",
"ORTHOMODE",
"SNAPMODE",
"SNAPUNIT",
"GRIDMODE",
"GRIDUNIT",
"GRIDMAJOR",
"DYNMODE",
"DYNPIFORMAT",
"DYNPICOORDS",
"DYNPROMPT",
"DYNDIGRIP",
"DYNDIVIS",
"OSNAPHATCH",
"LWDISPLAY",
"QPMODE",
"SELECTIONEFFECT",
"SELECTIONPREVIEW",
"PREVIEWEFFECT",
"PREVIEWFILTER",
"GRIPS",
"GRIPOBJLIMIT",
"SELECTIONAREA",
"WINDOWAREACOLOR",
"CROSSINGAREACOLOR",
"SELECTIONAREAOPACITY",
"PICKSTYLE",
"PICKBOX",
"APERTURE",
"GRIPSIZE",
"CURSORSIZE",
"PICKFIRST",
"PICKADD",
"PICKDRAG",
"MAXARRAY",
"MEASUREINIT",
];
/// The most profile entries [`restore`] reads (a saved profile is untrusted input).
const MAX_ENTRIES: usize = 256;
pub fn specs() -> Vec<CommandSpec> {
vec![
CommandSpec::new("options", "Options...", run_options)
.menu(&["Tools", "Options..."])
.alias(&["op", "preferences"])
.params("{reset?: bool, OSMODE?, PICKBOX?, APERTURE?, GRIPSIZE?, CURSORSIZE?, PICKFIRST?, PICKADD?, PICKDRAG?: 0 (two clicks) | 2 (clicks or drag), …: any profile variable}")
.enabled(always)
.noundo(),
]
}
/// Whether `name` is a profile variable (any case).
pub fn is_profile_var(name: &str) -> bool {
PROFILE_VARS.iter().any(|v| v.eq_ignore_ascii_case(name.trim()))
}
/// Every profile variable and its value, by name.
pub fn profile(s: &Session) -> Value {
let mut m = Map::new();
for name in PROFILE_VARS {
if let Some(v) = crate::sysvars::get(s, name) {
m.insert((*name).to_string(), v);
}
}
Value::Object(m)
}
/// Set the profile variables named in `values` (an object; other keys are refused). All or
/// nothing: one bad name or value leaves every setting as it was.
pub fn apply(s: &mut Session, values: &Value) -> Result<()> {
let Some(obj) = values.as_object() else { return Err(bad("options", "expected an object of profile variables")) };
let before = s.settings.clone();
for (name, v) in obj {
if !is_profile_var(name) {
s.settings = before;
return Err(bad("options", format!("{name} is not a profile setting")));
}
if let Err(e) = crate::sysvars::set(s, name, v) {
s.settings = before;
return Err(bad("options", format!("invalid value for {}: {e}", name.to_ascii_uppercase())));
}
}
Ok(())
}
/// Restore a saved [`profile`]: known variables with valid values are applied, anything else is
/// skipped (an old or hand-edited profile never stops the app from starting).
pub fn restore(s: &mut Session, saved: &Value) {
let Some(obj) = saved.as_object() else { return };
for (name, v) in obj.iter().take(MAX_ENTRIES) {
if is_profile_var(name) {
let before = s.settings.clone();
if crate::sysvars::set(s, name, v).is_err() {
s.settings = before;
}
}
}
}
/// Put every profile variable back to its default.
pub fn reset(s: &mut Session) {
let d = Settings::default();
let st = &mut s.settings;
st.osmode = d.osmode;
st.polarmode = d.polarmode;
st.otrack = d.otrack;
st.polar_flags = d.polar_flags;
st.polarang = d.polarang;
st.polaraddang = d.polaraddang;
st.orthomode = d.orthomode;
st.snapmode = d.snapmode;
st.gridmode = d.gridmode;
st.gridmajor = d.gridmajor;
st.dynmode = d.dynmode;
st.dynpi_cartesian = d.dynpi_cartesian;
st.dynpi_absolute = d.dynpi_absolute;
st.osnaphatch = d.osnaphatch;
st.lwdisplay = d.lwdisplay;
st.qpmode = d.qpmode;
st.pickbox = d.pickbox;
st.aperture = d.aperture;
st.gripsize = d.gripsize;
st.cursorsize = d.cursorsize;
st.pickfirst = d.pickfirst;
st.pickadd = d.pickadd;
st.pickdrag = d.pickdrag;
st.maxarray = d.maxarray;
}
fn run_options(s: &mut Session, p: &Value) -> Result<Value> {
// Typed without the desktop app's dialog (the CLI, scripts): report how to change them.
if p.is_null() {
let mut v = profile(s);
if let Some(m) = v.as_object_mut() {
m.insert("message".into(), json!("Options: change profile settings with SETVAR, or OPTIONS {\"NAME\": value}"));
}
return Ok(v);
}
let Some(obj) = p.as_object() else { return Err(bad("options", "expected an object")) };
let reset_first = match obj.get("reset") {
None | Some(Value::Null) => false,
Some(Value::Bool(b)) => *b,
Some(_) => return Err(bad("options", "`reset` must be true or false")),
};
let values: Map<String, Value> = obj.iter().filter(|(k, _)| k.as_str() != "reset").map(|(k, v)| (k.clone(), v.clone())).collect();
let before = s.settings.clone();
if reset_first {
reset(s);
}
if let Err(e) = apply(s, &Value::Object(values)) {
s.settings = before;
return Err(e);
}
s.touch();
Ok(profile(s))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_profile_var_round_trips_through_restore() {
let mut a = Session::new();
apply(&mut a, &json!({"PICKBOX": 7, "OSMODE": 3, "AUTOSNAP": 37, "POLARANG": 30, "SNAPUNIT": [2, 3], "DYNPIFORMAT": 1})).unwrap();
let saved = profile(&a);
for name in PROFILE_VARS {
assert!(saved.get(*name).is_some(), "{name} is reported");
}
let mut b = Session::new();
restore(&mut b, &saved);
assert_eq!(profile(&b), saved);
}
}