From f8f13c3f4e6c67dc4d3153b3a4c1ec23cd50b39d Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:49:28 +0200 Subject: [PATCH] Report DWGPREFIX, DWGTITLED, FILEDIA, CMDECHO and more from GETVAR (#1715) (#1742) * Report DWGPREFIX, DWGTITLED, FILEDIA, CMDECHO and more from GETVAR (#1715) * Keep CMDECHO and FILEDIA in session settings so they survive drawing switches (#1715) * Leave LOGINNAME out of GETVAR so scripts and agents can't read the user's login name Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/engine/src/lib.rs | 6 +++++ crates/engine/src/lisp/tests.rs | 18 +++++++++++++ crates/engine/src/sysvars.rs | 48 +++++++++++++++++++++++++++++++-- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 1bf8e0059..9435e4293 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -400,6 +400,10 @@ pub struct Settings { pub pickfirst: bool, /// ACADLSPASDOC: acad.lsp loads into every drawing, not once per session. pub acadlspasdoc: bool, + /// CMDECHO: kept for routines that switch command echo around their commands; no command reads it yet. + pub cmdecho: bool, + /// FILEDIA: kept for routines that switch file dialogs off around a script; no command reads it yet. + pub filedia: bool, pub pickadd: bool, /// PICKDRAG: false = two-click windows (0), true = clicks or dragging (2). /// Drag-only mode (1) is not supported yet. @@ -491,6 +495,8 @@ impl Default for Settings { aperture: 10.0, pickfirst: true, acadlspasdoc: false, + cmdecho: true, + filedia: true, pickadd: true, pickdrag: true, pickstyle: 1, diff --git a/crates/engine/src/lisp/tests.rs b/crates/engine/src/lisp/tests.rs index 9104e2525..d6bd1a70d 100644 --- a/crates/engine/src/lisp/tests.rs +++ b/crates/engine/src/lisp/tests.rs @@ -437,6 +437,24 @@ fn tables_and_variables() { assert_eq!(ok(&mut s, "(getvar \"NOSUCHVAR\")"), "nil"); } +#[test] +fn getvar_drawing_and_session_variables() { + let mut s = session(); + assert_eq!(ok(&mut s, "(list (getvar \"DWGPREFIX\") (getvar \"DWGTITLED\"))"), "(\"\" 0)"); + assert_eq!(ok(&mut s, "(list (getvar \"FILEDIA\") (getvar \"CMDECHO\") (getvar \"TILEMODE\"))"), "(1 1 1)"); + assert_eq!(ok(&mut s, "(setvar \"FILEDIA\" 0) (setvar \"CMDECHO\" 0) (getvar \"FILEDIA\")"), "0"); + // Both belong to the session, so they survive a switch to another drawing. + s.new_drawing(true); + assert_eq!(ok(&mut s, "(list (getvar \"FILEDIA\") (getvar \"CMDECHO\"))"), "(0 0)"); + assert_eq!(ok(&mut s, "(getvar \"CMDACTIVE\")"), "0"); + let dir = std::env::temp_dir(); + let file = dir.join("lisp_getvar_test.dxf"); + s.state_mut().unwrap().path = Some(file.to_string_lossy().into_owned()); + let want = format!("{}{}", dir.to_string_lossy().trim_end_matches(std::path::MAIN_SEPARATOR), std::path::MAIN_SEPARATOR); + assert_eq!(crate::sysvars::get(&s, "DWGPREFIX"), Some(serde_json::json!(want))); + assert_eq!(ok(&mut s, "(getvar \"DWGTITLED\")"), "1"); +} + #[test] fn input_functions_take_supplied_answers() { let mut s = session(); diff --git a/crates/engine/src/sysvars.rs b/crates/engine/src/sysvars.rs index dc8e0ab2a..b77f04656 100644 --- a/crates/engine/src/sysvars.rs +++ b/crates/engine/src/sysvars.rs @@ -55,6 +55,8 @@ const SESSION_VARS: &[&str] = &[ "APERTURE", "PICKFIRST", "ACADLSPASDOC", + "CMDECHO", + "FILEDIA", "PICKADD", "PICKDRAG", "PICKSTYLE", @@ -129,6 +131,8 @@ pub fn get(s: &Session, name: &str) -> Option { "APERTURE" => json!(st.aperture), "PICKFIRST" => json!(i32::from(st.pickfirst)), "ACADLSPASDOC" => json!(i32::from(st.acadlspasdoc)), + "CMDECHO" => json!(i32::from(st.cmdecho)), + "FILEDIA" => json!(i32::from(st.filedia)), "PICKADD" => json!(i32::from(st.pickadd)), "PICKDRAG" => json!(i32::from(st.pickdrag) * 2), "PICKSTYLE" => json!(st.pickstyle), @@ -149,6 +153,12 @@ pub fn get(s: &Session, name: &str) -> Option { "CMDNAMES" => json!(s.running.as_ref().map(|r| r.id.to_ascii_uppercase()).unwrap_or_default()), "DWGNAME" => json!(s.state().map(|d| d.title.clone()).unwrap_or_default()), "DBMOD" => json!(s.state().map(|d| i32::from(d.is_dirty())).unwrap_or(0)), + // The drawing's folder with a trailing separator ("" while it has no file); DWGTITLED is 1 once it has one. + "DWGPREFIX" => json!(s.state().ok().and_then(|d| d.path.as_deref()).map(dwg_prefix).unwrap_or_default()), + "DWGTITLED" => json!(s.state().map(|d| i32::from(d.path.is_some())).unwrap_or(0)), + "CMDACTIVE" => json!(i32::from(s.running.is_some())), + "TILEMODE" => json!(i32::from(s.state().is_ok_and(|d| d.space.layout().is_none()))), + "PLATFORM" => json!(std::env::consts::OS), "BLOCKEDITOR" => json!(i32::from(s.state().is_ok_and(|d| d.block_edit.is_some()))), // The template NEW/QNEW start from (a profile setting, like FONTALT; "" = none). "QNEWTEMPLATE" => json!(crate::cmd::file::qnew_template()), @@ -186,6 +196,21 @@ pub fn get(s: &Session, name: &str) -> Option { Some(v) } +/// DWGPREFIX: the folder of the drawing file, made absolute, with a trailing separator. +fn dwg_prefix(path: &str) -> String { + let full = std::path::absolute(path).unwrap_or_else(|_| std::path::PathBuf::from(path)); + match full.parent() { + Some(dir) => { + let mut text = dir.to_string_lossy().into_owned(); + if !text.ends_with(std::path::MAIN_SEPARATOR) { + text.push(std::path::MAIN_SEPARATOR); + } + text + } + None => String::new(), + } +} + fn as_bool(v: &Value) -> Option { v.as_bool().or_else(|| v.as_i64().map(|i| i != 0)).or_else(|| v.as_str().and_then(|s| s.trim().parse::().ok()).map(|i| i != 0)) } @@ -205,8 +230,25 @@ fn as_pt(v: &Value) -> Option { } /// Variables `get` reports from session or drawing state that `set` can't change. -const READ_ONLY: &[&str] = - &["CMDNAMES", "DWGNAME", "DBMOD", "BLOCKEDITOR", "CTAB", "LASTPOINT", "VIEWCTR", "VIEWSIZE", "EXTMIN", "EXTMAX", "UNDOCTL", "UNDOMARKS"]; +const READ_ONLY: &[&str] = &[ + "CMDNAMES", + "DWGNAME", + "DWGPREFIX", + "DWGTITLED", + "CMDACTIVE", + "TILEMODE", + "PLATFORM", + "DBMOD", + "BLOCKEDITOR", + "CTAB", + "LASTPOINT", + "VIEWCTR", + "VIEWSIZE", + "EXTMIN", + "EXTMAX", + "UNDOCTL", + "UNDOMARKS", +]; pub fn is_read_only(name: &str) -> bool { READ_ONLY.iter().any(|r| r.eq_ignore_ascii_case(name.trim())) || table::is_read_only(&name.trim().to_ascii_uppercase()) @@ -302,6 +344,8 @@ pub fn set(s: &mut Session, name: &str, v: &Value) -> Result<()> { "APERTURE" => st.aperture = as_f64(v).ok_or_else(bad)?.clamp(1.0, 50.0), "PICKFIRST" => st.pickfirst = as_bool(v).ok_or_else(bad)?, "ACADLSPASDOC" => st.acadlspasdoc = as_bool(v).ok_or_else(bad)?, + "CMDECHO" => st.cmdecho = small(v, 1).ok_or_else(bad)? == 1, + "FILEDIA" => st.filedia = small(v, 1).ok_or_else(bad)? == 1, "PICKADD" => st.pickadd = as_bool(v).ok_or_else(bad)?, "PICKDRAG" => st.pickdrag = as_i(v).filter(|b| matches!(b, 0 | 2)).map(|b| b == 2).ok_or_else(bad)?, "PICKSTYLE" => st.pickstyle = as_i(v).filter(|b| (0..=3).contains(b)).and_then(|b| u8::try_from(b).ok()).ok_or_else(bad)?,