diff --git a/assets/ocad.pgp b/assets/ocad.pgp new file mode 100644 index 000000000..2d90217c4 --- /dev/null +++ b/assets/ocad.pgp @@ -0,0 +1,124 @@ +; OpenCADStudio command aliases. +; Format: ALIAS,*COMMAND (lines starting with ';' are comments) +; Edit here or via the ALIASEDIT command. One alias per line. +; +; This file is the single source of truth for command-line aliases. It is +; copied to your user config folder on first launch; edit that copy (or use +; ALIASEDIT) to add, remap, or remove aliases. + +; ── Draw ────────────────────────────────────────────────────────────────── +L, *LINE +ML, *MLINE +PL, *PLINE +A, *ARC +C, *CIRCLE +DO, *DONUT +EL, *ELLIPSE +PO, *POINT +SPL, *SPLINE +H, *HATCH +HE, *HATCHEDIT +XL, *XLINE +POL, *POLYGON +REC, *RECT +SO, *SOLID +WO, *WIPEOUT + +; ── Modify ──────────────────────────────────────────────────────────────── +M, *MOVE +CO, *COPY +RO, *ROTATE +SC, *SCALE +MI, *MIRROR +E, *ERASE +X, *EXPLODE +SS, *STRETCH +O, *OFFSET +TR, *TRIM +EX, *EXTEND +F, *FILLET +CHA, *CHAMFER +J, *JOIN +BR, *BREAK +BAP, *BREAKATPOINT +PE, *PEDIT +SPE, *SPLINEDIT +LEN, *LENGTHEN +AL, *ALIGN +AR, *ARRAY +MA, *MATCHPROP +ATE, *ATTEDIT + +; ── Clipboard ───────────────────────────────────────────────────────────── +CC, *COPYCLIP +CX, *CUTCLIP +PC, *PASTECLIP + +; ── Blocks / references ─────────────────────────────────────────────────── +XA, *XATTACH +XR, *XREF +ADC, *ADCENTER + +; ── Layers / groups ─────────────────────────────────────────────────────── +LA, *LAYERS +G, *GROUP +UG, *UNGROUP + +; ── Dimensions / annotation ─────────────────────────────────────────────── +DAL, *DIMALIGNED +DAN, *DIMANGULAR +DDI, *DIMDIAMETER +DLI, *DIMLINEAR +DRA, *DIMRADIUS +DOR, *DIMORDINATE +DBA, *DIMBASELINE +DCO, *DIMCONTINUE +DED, *DIMEDIT +DBR, *DIMBREAK +DJL, *DIMJOGLINE +DJO, *DIMJOGGED +DCE, *DIMCENTER +LE, *LEADER +QL, *QLEADER +MLD, *MLEADER +MLA, *MLEADERADD +MLR, *MLEADERREMOVE +MLAL, *MLEADERALIGN +MLC, *MLEADERCOLLECT +MT, *MTEXT +DT, *TEXT +T, *TEXT +ED, *DDEDIT +TOL, *TOLERANCE + +; ── Inquiry / selection ─────────────────────────────────────────────────── +AA, *AREA +DI, *DIST +DIV, *DIVIDE +ME, *MEASURE +MEA, *MEASUREGEOM +LI, *LIST +SA, *SELECTALL +DE, *DESELECT +QS, *QSELECT +FI, *FILTER + +; ── View / display ──────────────────────────────────────────────────────── +P, *PAN +U, *UNDO +PR, *PROPERTIES +UN, *UNITS +TP, *TOOLPALETTES +SSM, *SHEETSET +VW, *WIREFRAME +HI, *HIDE +3O, *3DORBIT +MV, *MVIEW +MS, *MSPACE +PW, *PLOTWINDOW +QP, *QUICKPRINT + +; ── 3D solids ───────────────────────────────────────────────────────────── +EXT, *EXTRUDE +REV, *REVOLVE +INF, *INTERFERE diff --git a/src/app/alias.rs b/src/app/alias.rs new file mode 100644 index 000000000..740ef9b59 --- /dev/null +++ b/src/app/alias.rs @@ -0,0 +1,159 @@ +//! Command aliases — short abbreviations typed at the command line that expand +//! to a full command (e.g. `L` → `LINE`). The alias table is the single source +//! of truth: it is loaded from a user-editable `aliases.pgp` file at startup and +//! consulted before every command dispatch (see `resolve_alias`), so users can +//! add, remap, or delete aliases without touching the app. +//! +//! File format mirrors the long-standing plain-text `.pgp` convention so it is +//! familiar and hand-editable, stored as `ocad.pgp`: +//! +//! ```text +//! ; lines beginning with a semicolon are comments +//! L, *LINE +//! CC, *COPYCLIP +//! ``` +//! +//! An alias is the token left of the comma; the command is the token right of +//! it, with an optional leading `*`. Both are matched case-insensitively and +//! stored uppercased. Stored next to the other per-user config under +//! `crate::config::config_dir()`, so no serialization crate is pulled in. + +use super::OpenCADStudio; +use rustc_hash::FxHashMap; +use std::path::PathBuf; + +/// The shipped default alias file, embedded at compile time. Its aliases live in +/// `assets/ocad.pgp` — not in Rust — so the defaults are just data in the same +/// format the user edits. On first launch this text is written verbatim to the +/// user's config folder (comments and all) as the starting `ocad.pgp`; only that +/// user copy is read thereafter. This embedded copy is the first-run seed / +/// factory default. +const DEFAULT_ALIASES_PGP: &str = include_str!("../../assets/ocad.pgp"); + +/// Path to the user's alias file, `/ocad.pgp`. `None` when the platform +/// config base can't be resolved (headless, no `HOME`, wasm). +fn alias_file_path() -> Option { + Some(crate::config::config_dir()?.join("ocad.pgp")) +} + +/// Parse `.pgp` text into an `alias → command` map, both uppercased. Skips blank +/// lines and `;` comments; tolerates a leading `*` on the command and arbitrary +/// whitespace padding. +fn parse_pgp(body: &str) -> FxHashMap { + let mut map = FxHashMap::default(); + for line in body.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with(';') { + continue; + } + let Some((alias, cmd)) = line.split_once(',') else { + continue; + }; + let alias = alias.trim().to_uppercase(); + let cmd = cmd.trim().trim_start_matches('*').trim().to_uppercase(); + if alias.is_empty() || cmd.is_empty() { + continue; + } + map.insert(alias, cmd); + } + map +} + +/// Serialize an `alias → command` map to `.pgp` text with a header and aligned +/// columns, sorted by alias for a stable, diffable file. +pub(super) fn to_pgp(map: &FxHashMap) -> String { + let mut rows: Vec<(&String, &String)> = map.iter().collect(); + rows.sort_by(|a, b| a.0.cmp(b.0)); + let mut out = String::from( + "; OpenCADStudio command aliases.\n\ + ; Format: ALIAS,*COMMAND (lines starting with ';' are comments)\n\ + ; Edit here or via the ALIASEDIT command. One alias per line.\n\n", + ); + for (alias, cmd) in rows { + out.push_str(&format!("{:<10}*{}\n", format!("{alias},"), cmd)); + } + out +} + +/// The built-in defaults, parsed from the embedded `.pgp`. Used when no user +/// file exists yet (or can't be reached, e.g. wasm). +fn default_map() -> FxHashMap { + parse_pgp(DEFAULT_ALIASES_PGP) +} + +/// Load the alias table at boot. Reads the user's `aliases.pgp`; if it is +/// missing, writes the shipped default file verbatim (preserving its comments +/// and layout) so the user has a well-formatted starting point to edit, then +/// parses it. Falls back to the embedded defaults when the file can't be read +/// or written (read-only home, wasm — where it silently no-ops). +pub(super) fn load_aliases() -> FxHashMap { + match alias_file_path() { + Some(path) => match std::fs::read_to_string(&path) { + Ok(body) => parse_pgp(&body), + Err(_) => { + // No file yet — copy the shipped default file, best-effort. + if let Some(dir) = path.parent() { + let _ = std::fs::create_dir_all(dir); + } + let _ = std::fs::write(&path, DEFAULT_ALIASES_PGP); + default_map() + } + }, + None => default_map(), + } +} + +/// Persist the alias table to `aliases.pgp`. Best-effort; returns `Ok` (no-op) +/// when there is no config dir (wasm/headless). +pub(super) fn save_map(map: &FxHashMap) -> std::io::Result<()> { + let Some(path) = alias_file_path() else { + return Ok(()); + }; + if let Some(dir) = path.parent() { + std::fs::create_dir_all(dir)?; + } + std::fs::write(path, to_pgp(map)) +} + +impl OpenCADStudio { + /// Rewrite the leading command token through the alias table, leaving any + /// arguments after the first whitespace untouched. Returns `None` when the + /// verb is not an alias, so the caller passes the original string through + /// unchanged. Keyed uppercase so lowercase aliases from scripts/plugins + /// (which bypass the command line's verb-uppercasing) still resolve. + pub(super) fn resolve_alias(&self, cmd: &str) -> Option { + if self.command_aliases.is_empty() { + return None; + } + let (verb, rest) = match cmd.split_once(char::is_whitespace) { + Some((v, r)) => (v, Some(r)), + None => (cmd, None), + }; + let canon = self.command_aliases.get(&verb.to_uppercase())?; + Some(match rest { + Some(r) => format!("{canon} {r}"), + None => canon.clone(), + }) + } + + /// Replace the alias table (from the editor), persist it, and mirror it into + /// the command line so autocomplete reflects the edits immediately. + pub(super) fn set_command_aliases(&mut self, map: FxHashMap) { + let _ = save_map(&map); + self.command_line.command_aliases = map.clone(); + self.command_aliases = map; + } + + /// Commit the alias-editor working rows to the table (Apply button): build a + /// map from the rows, dropping incomplete ones, and persist + sync. Leaves + /// the editor open so the user can keep editing. + pub(super) fn apply_alias_editor_rows(&mut self) { + let map: FxHashMap = self + .alias_editor_rows + .iter() + .filter(|(a, c)| !a.trim().is_empty() && !c.trim().is_empty()) + .map(|(a, c)| (a.trim().to_string(), c.trim().to_string())) + .collect(); + self.set_command_aliases(map); + } +} diff --git a/src/app/commands/blocks.rs b/src/app/commands/blocks.rs index bffb43c44..44c6f824c 100644 --- a/src/app/commands/blocks.rs +++ b/src/app/commands/blocks.rs @@ -53,7 +53,7 @@ impl OpenCADStudio { } } - "COPYCLIP" | "CC" => { + "COPYCLIP" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() @@ -142,7 +142,7 @@ impl OpenCADStudio { } } - "CUTCLIP" | "CX" => { + "CUTCLIP" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() @@ -179,7 +179,7 @@ impl OpenCADStudio { } } - "PASTECLIP" | "PC" => { + "PASTECLIP" => { if self.clipboard.is_empty() { self.command_line.push_error("Clipboard is empty."); } else { @@ -386,7 +386,7 @@ impl OpenCADStudio { // ADCENTER / CONTENTBROWSER — report the drawing's named content // (blocks and layers) from the command line in place of the browser // panel. - "ADCENTER" | "CONTENTBROWSER" | "ADC" => { + "ADCENTER" | "CONTENTBROWSER" => { let blocks = self.tabs[i].scene.custom_block_names(); let layers: Vec = self.tabs[i] .scene @@ -476,7 +476,7 @@ impl OpenCADStudio { } } - "XATTACH" | "XA" => { + "XATTACH" => { // Launch the file picker; XAttachPickResult will start the command. return Some(Task::done(Message::XAttachPick)); } @@ -498,7 +498,7 @@ impl OpenCADStudio { } } - "XREF" | "XR" => { + "XREF" => { // List all xref blocks in the current drawing. let xrefs: Vec = self.tabs[i] .scene diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index b0da3404d..50bf761ea 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -3,42 +3,42 @@ use super::*; impl OpenCADStudio { pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option> { match cmd { - "DIMALIGNED" | "DAL" => { + "DIMALIGNED" => { use crate::modules::annotate::aligned_dim::AlignedDimensionCommand; let cmd = AlignedDimensionCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "DIMDIAMETER" | "DDI" => { + "DIMDIAMETER" => { use crate::modules::annotate::diameter_dim::DiameterDimensionCommand; let cmd = DiameterDimensionCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "DIMLINEAR" | "DLI" => { + "DIMLINEAR" => { use crate::modules::annotate::linear_dim::LinearDimensionCommand; let new_cmd = LinearDimensionCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "DIMRADIUS" | "DRA" => { + "DIMRADIUS" => { use crate::modules::annotate::radius_dim::RadiusDimensionCommand; let new_cmd = RadiusDimensionCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "DIMANGULAR" | "DAN" => { + "DIMANGULAR" => { use crate::modules::annotate::angular_dim::AngularDimensionCommand; let new_cmd = AngularDimensionCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "DIMORDINATE" | "DOR" => { + "DIMORDINATE" => { use crate::modules::annotate::ordinate_dim::OrdinateDimCommand; let new_cmd = OrdinateDimCommand::new(); self.command_line.push_info(&new_cmd.prompt()); @@ -47,7 +47,7 @@ impl OpenCADStudio { // LEADER draws a leader line with an annotation; QLEADER is the same // quick leader-plus-annotation operation. - "LEADER" | "LE" | "QLEADER" | "QL" => { + "LEADER" | "QLEADER" => { use crate::modules::annotate::leader_cmd::LeaderCommand; let new_cmd = LeaderCommand::new(); self.command_line.push_info(&new_cmd.prompt()); @@ -400,14 +400,14 @@ impl OpenCADStudio { )); } - "MLEADER" | "MLD" => { + "MLEADER" => { use crate::modules::annotate::mleader_cmd::MLeaderCommand; let new_cmd = MLeaderCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "TOLERANCE" | "TOL" => { + "TOLERANCE" => { use crate::modules::annotate::tolerance_cmd::ToleranceCommand; let cmd = ToleranceCommand::new(); self.command_line.push_info(&cmd.prompt()); @@ -421,7 +421,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "DIMCONTINUE" | "DCO" => { + "DIMCONTINUE" => { use crate::modules::annotate::dim_continue::DimContinueCommand; let cmd = if let Some((p1, p2, dp, rot, trot)) = find_last_linear_dim(&self.tabs[i].scene) @@ -434,7 +434,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "DIMBASELINE" | "DBA" => { + "DIMBASELINE" => { use crate::modules::annotate::dim_baseline::DimBaselineCommand; let cmd = if let Some((p1, p2, dp, rot, trot)) = find_last_linear_dim(&self.tabs[i].scene) @@ -548,7 +548,7 @@ impl OpenCADStudio { return Some(Task::none()); } - "DIMEDIT" | "DED" => { + "DIMEDIT" => { use crate::modules::annotate::dimedit::DimEditCommand; let cmd = DimEditCommand::new(); self.command_line.push_info(&cmd.prompt()); @@ -562,7 +562,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "DIMBREAK" | "DBR" => { + "DIMBREAK" => { use crate::modules::annotate::dimbreak::DimBreakCommand; let cmd = DimBreakCommand::new(); self.command_line.push_info(&cmd.prompt()); @@ -576,35 +576,35 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "DIMJOGLINE" | "DJL" => { + "DIMJOGLINE" => { use crate::modules::annotate::dimjogline::DimJogLineCommand; let cmd = DimJogLineCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "MLEADERADD" | "MLA" => { + "MLEADERADD" => { use crate::modules::annotate::mleader_edit::MLeaderAddCommand; let cmd = MLeaderAddCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "MLEADERREMOVE" | "MLR" => { + "MLEADERREMOVE" => { use crate::modules::annotate::mleader_edit::MLeaderRemoveCommand; let cmd = MLeaderRemoveCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "MLEADERALIGN" | "MLAL" => { + "MLEADERALIGN" => { use crate::modules::annotate::mleader_edit::MLeaderAlignCommand; let cmd = MLeaderAlignCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "MLEADERCOLLECT" | "MLC" => { + "MLEADERCOLLECT" => { use crate::modules::annotate::mleader_edit::MLeaderCollectCommand; let cmd = MLeaderCollectCommand::new(); self.command_line.push_info(&cmd.prompt()); @@ -647,14 +647,14 @@ impl OpenCADStudio { } } - "PLOTWINDOW" | "PW" => { + "PLOTWINDOW" => { use crate::modules::view::plot_window::PlotWindowCommand; let cmd = PlotWindowCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "QUICKPRINT" | "QP" => { + "QUICKPRINT" => { use crate::modules::view::quick_print::QuickPrintCommand; let cmd = QuickPrintCommand::new(); self.command_line.push_info(&cmd.prompt()); @@ -671,7 +671,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "STRETCH" | "SS" => { + "STRETCH" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() @@ -692,7 +692,7 @@ impl OpenCADStudio { } } - "FILLET" | "F" => { + "FILLET" => { use crate::modules::draw::modify::fillet::FilletCommand; let entities: Vec<_> = self.tabs[i] .scene @@ -717,7 +717,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "ARRAY" | "AR" | "ARRAYRECT" => { + "ARRAY" | "ARRAYRECT" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() @@ -809,7 +809,7 @@ impl OpenCADStudio { } } - "CHAMFER" | "CHA" => { + "CHAMFER" => { use crate::modules::draw::modify::fillet::ChamferCommand; let entities: Vec<_> = self.tabs[i] .scene @@ -834,7 +834,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "EXPLODE" | "X" => { + "EXPLODE" => { use crate::modules::draw::modify::explode::explode_entity; let entities: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect(); if entities.is_empty() { @@ -876,7 +876,7 @@ impl OpenCADStudio { } } - "OFFSET" | "O" => { + "OFFSET" => { use crate::modules::draw::modify::offset::OffsetCommand; let all_entities: Vec<_> = self.tabs[i] .scene @@ -892,7 +892,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "TRIM" | "TR" => { + "TRIM" => { use crate::modules::draw::modify::trim::TrimCommand; let entities: Vec<_> = self.tabs[i] .scene @@ -930,7 +930,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "EXTEND" | "EX" => { + "EXTEND" => { use crate::modules::draw::modify::trim::ExtendCommand; let entities: Vec<_> = self.tabs[i] .scene @@ -955,7 +955,7 @@ impl OpenCADStudio { // DIMJOGGED — create a jogged radius dimension on the selected // arc/circle: a standard radius dim carrying an OCS_JOGGED XData marker // that the geometry generator renders with a foreshortened zig-zag. - "DIMJOGGED" | "DJO" | "DIMJOG" => { + "DIMJOGGED" | "DIMJOG" => { use acadrust::entities::{Dimension, DimensionRadius}; use acadrust::types::Vector3; use acadrust::xdata::{ExtendedDataRecord, XDataValue}; diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index 8e21cf6f2..d109c2549 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -5,7 +5,7 @@ impl OpenCADStudio { match cmd { // Interactive pan: left-drag pans the view until Esc. The only pan // path when there is no middle mouse button (trackpad / web). - "PAN" | "P" => { + "PAN" => { self.tabs[i].pan_mode = true; self.command_line .push_output("PAN: drag with the left mouse button. Press Esc to exit."); @@ -126,7 +126,7 @@ impl OpenCADStudio { } // ── PROPERTIES — toggle Properties panel visibility ────────────────── - "PROPERTIES" | "PR" | "PROPS" => { + "PROPERTIES" | "PROPS" => { return Some(Task::done(Message::ToggleProperties)); } @@ -160,7 +160,7 @@ impl OpenCADStudio { return Some(Task::done(Message::ToggleSnapPopup)); } // UNITS — open the drawing-units picker (linear / angular format). - "UNITS" | "UN" | "DDUNITS" => { + "UNITS" | "DDUNITS" => { return Some(Task::done(Message::ToggleUnitsPopup)); } @@ -174,13 +174,13 @@ impl OpenCADStudio { } // ── TOOLPALETTES — not yet implemented ─────────────────────────────── - "TOOLPALETTES" | "TP" => { + "TOOLPALETTES" => { self.command_line .push_info("TOOLPALETTES: Tool Palettes not yet implemented."); } // ── SHEETSET — not yet implemented ─────────────────────────────────── - "SHEETSET" | "SSM" => { + "SHEETSET" => { self.command_line .push_info("SHEETSET: Sheet Set Manager not yet implemented."); } @@ -292,7 +292,7 @@ impl OpenCADStudio { // PRESSPULL on a closed boundary creates a solid by extruding it to a // height — the same operation as EXTRUDE. THICKEN turns a closed planar // profile into a solid of the given thickness, which is also an extrude. - "EXTRUDE" | "EXT" | "PRESSPULL" | "THICKEN" => { + "EXTRUDE" | "PRESSPULL" | "THICKEN" => { use crate::modules::insert::solid3d_cmds::ExtrudeCommand; // If a single entity is already selected, skip the pick step. let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect(); @@ -311,7 +311,7 @@ impl OpenCADStudio { } // ── REVOLVE ──────────────────────────────────────────────────── - "REVOLVE" | "REV" => { + "REVOLVE" => { use crate::modules::insert::solid3d_cmds::RevolveCommand; let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); let cmd = RevolveCommand::new(color); diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 30f1d2035..9b67e69c9 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -4,14 +4,14 @@ impl OpenCADStudio { pub(super) fn dispatch_draw(&mut self, cmd: &str, i: usize) -> Option> { match cmd { // ── Draw commands ────────────────────────────────────────────── - "LINE" | "L" => { + "LINE" => { use crate::modules::draw::draw::line::LineCommand; let new_cmd = LineCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "MLINE" | "ML" => { + "MLINE" => { use crate::modules::draw::draw::mline::MlineCommand; let style = self.tabs[i].scene.document.header.multiline_style.clone(); let cmd_obj = MlineCommand::with_style(style); @@ -184,14 +184,14 @@ impl OpenCADStudio { } } - "DONUT" | "DO" => { + "DONUT" => { use crate::modules::draw::draw::donut::DonutCommand; let cmd = DonutCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "CIRCLE" | "C" => { + "CIRCLE" => { use crate::modules::draw::draw::circle::CircleCommand; let new_cmd = CircleCommand::new(); self.command_line.push_info(&new_cmd.prompt()); @@ -232,7 +232,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "ARC" | "A" => { + "ARC" => { use crate::modules::draw::draw::arc::ArcCommand; let new_cmd = ArcCommand::new(); self.command_line.push_info(&new_cmd.prompt()); @@ -318,7 +318,7 @@ impl OpenCADStudio { } } - "RECT" | "RECTANG" | "REC" => { + "RECT" | "RECTANG" => { use crate::modules::draw::draw::shapes::RectCommand; let new_cmd = RectCommand::new(); self.command_line.push_info(&new_cmd.prompt()); @@ -348,7 +348,7 @@ impl OpenCADStudio { } self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "POLY" | "POLYGON" | "POL" => { + "POLY" | "POLYGON" => { use crate::modules::draw::draw::shapes::PolyCommand; let new_cmd = PolyCommand::new(); self.command_line.push_info(&new_cmd.prompt()); @@ -367,7 +367,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "PLINE" | "PL" => { + "PLINE" => { use crate::modules::draw::draw::polyline::PlineCommand; let new_cmd = PlineCommand::new(); self.command_line.push_info(&new_cmd.prompt()); @@ -383,7 +383,7 @@ impl OpenCADStudio { // 2D filled solid. Reached via SO / SOLID2D — the bare SOLID verb is // currently the shaded-display toggle (token collision tracked). - "SO" | "SOLID" | "SOLID2D" => { + "SOLID" | "SOLID2D" => { use crate::modules::draw::draw::solid2d::Solid2dCommand; let new_cmd = Solid2dCommand::new(); self.command_line.push_info(&new_cmd.prompt()); @@ -411,7 +411,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "DIMCENTER" | "DCE" | "CENTERMARK" => { + "DIMCENTER" | "CENTERMARK" => { use crate::modules::draw::draw::dimcenter::DimCenterCommand; let new_cmd = DimCenterCommand::new(); self.command_line.push_info(&new_cmd.prompt()); @@ -432,7 +432,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "MEASUREGEOM" | "MEA" => { + "MEASUREGEOM" => { use crate::modules::draw::inquiry::measuregeom::MeasureGeomCommand; let new_cmd = MeasureGeomCommand::new(); self.command_line.push_info(&new_cmd.prompt()); @@ -442,7 +442,7 @@ impl OpenCADStudio { // ── Modify commands ──────────────────────────────────────────── // MOVE works from picked points, so it already relocates entities // in 3D; 3DMOVE is the same operation. - "MOVE" | "M" | "3DMOVE" => { + "MOVE" | "3DMOVE" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() @@ -463,7 +463,7 @@ impl OpenCADStudio { } } - "COPY" | "CO" => { + "COPY" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() @@ -484,7 +484,7 @@ impl OpenCADStudio { } } - "ROTATE" | "RO" => { + "ROTATE" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() @@ -539,7 +539,7 @@ impl OpenCADStudio { } } - "POINT" | "PO" => { + "POINT" => { use crate::modules::draw::draw::point::PointCommand; let new_cmd = PointCommand::new(); self.command_line.push_info(&new_cmd.prompt()); @@ -553,14 +553,14 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "XLINE" | "XL" | "CONSTRUCTIONLINE" => { + "XLINE" | "CONSTRUCTIONLINE" => { use crate::modules::draw::draw::ray::XLineCommand; let new_cmd = XLineCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "HATCH" | "H" => { + "HATCH" => { use crate::modules::draw::draw::hatch::HatchCommand; let outlines = self.tabs[i].scene.closed_outlines(); let new_cmd = HatchCommand::new(outlines); @@ -568,7 +568,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "HATCHEDIT" | "HE" => { + "HATCHEDIT" => { use crate::modules::draw::draw::hatchedit::HatcheditCommand; // If a single hatch is already selected, skip the pick step. let sel = self.tabs[i].scene.selected_entities(); @@ -610,7 +610,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "ELLIPSE" | "EL" => { + "ELLIPSE" => { use crate::modules::draw::draw::ellipse::EllipseCommand; let new_cmd = EllipseCommand::new(); self.command_line.push_info(&new_cmd.prompt()); @@ -631,14 +631,14 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "SPLINE" | "SPL" => { + "SPLINE" => { use crate::modules::draw::draw::spline::SplineCommand; let new_cmd = SplineCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "SCALE" | "SC" => { + "SCALE" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() @@ -659,7 +659,7 @@ impl OpenCADStudio { } } - "MIRROR" | "MI" => { + "MIRROR" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() @@ -682,7 +682,7 @@ impl OpenCADStudio { } } - "ERASE" | "E" => { + "ERASE" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() @@ -727,7 +727,7 @@ impl OpenCADStudio { } // INTERFERE — non-destructive intersect: solid from the overlap. - "INTERFERE" | "INF" => { + "INTERFERE" => { return Some(self.solid_interfere()); } @@ -963,14 +963,14 @@ impl OpenCADStudio { } // ── Annotate commands ────────────────────────────────────────── - "TEXT" | "T" | "DT" => { + "TEXT" => { use crate::modules::annotate::text::TextCommand; let new_cmd = TextCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "DDEDIT" | "ED" => { + "DDEDIT" => { use crate::modules::annotate::ddedit::DdeditCommand; // A single text entity already selected opens its in-place // editor directly; otherwise prompt for a pick. @@ -994,7 +994,7 @@ impl OpenCADStudio { } } - "MTEXT" | "MT" => { + "MTEXT" => { use crate::modules::annotate::mtext::MTextCommand; let new_cmd = MTextCommand::new(); self.command_line.push_info(&new_cmd.prompt()); diff --git a/src/app/commands/fileops.rs b/src/app/commands/fileops.rs index 69a700dc0..615795df7 100644 --- a/src/app/commands/fileops.rs +++ b/src/app/commands/fileops.rs @@ -70,7 +70,7 @@ impl OpenCADStudio { } } } - "UNDO" | "U" => return Some(Task::done(Message::Undo)), + "UNDO" => return Some(Task::done(Message::Undo)), "REDO" => return Some(Task::done(Message::Redo)), // OOPS — restore the objects removed by the most recent ERASE, // without undoing any work done since. @@ -92,7 +92,7 @@ impl OpenCADStudio { } } "CLEAR" | "CLR" => return Some(Task::done(Message::ClearScene)), - "WIREFRAME" | "VW" => return Some(Task::done(Message::SetWireframe(true))), + "WIREFRAME" => return Some(Task::done(Message::SetWireframe(true))), // Visual-style commands. OCS renders either a wireframe or a shaded // view; the named styles map onto the closest of the two and the // chosen style is reported so the mapping is explicit. (`SOLID` is @@ -309,7 +309,7 @@ impl OpenCADStudio { "ORTHO" => return Some(Task::done(Message::ToggleOrtho)), "PARALLEL" => return Some(Task::done(Message::SetProjection(true))), "PERSP" => return Some(Task::done(Message::SetProjection(false))), - "LAYERS" | "LA" => return Some(Task::done(Message::ToggleLayers)), + "LAYERS" => return Some(Task::done(Message::ToggleLayers)), // SCRIPT — run a command script: each non-blank, non-comment // line is fed through the same command path the `--script` startup diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index 548045271..2e5f0712b 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -3,13 +3,13 @@ use super::*; impl OpenCADStudio { pub(super) fn dispatch_inquiry(&mut self, cmd: &str, i: usize) -> Option> { match cmd { - "3DORBIT" | "3O" => { + "3DORBIT" => { self.command_line .push_info("3D Orbit: drag with right mouse button."); } // ── Selection utilities ─────────────────────────────────────── - "SELECTALL" | "SA" => { + "SELECTALL" => { use crate::scene::Scene; let handles: Vec = self.tabs[i] .scene @@ -28,7 +28,7 @@ impl OpenCADStudio { self.refresh_properties(); } - "DESELECT" | "DE" | "DESELALL" => { + "DESELECT" | "DESELALL" => { self.tabs[i].scene.deselect_all(); self.command_line.push_output("Deselected."); self.refresh_properties(); @@ -49,7 +49,7 @@ impl OpenCADStudio { // QSELECT builds a selection set by object type / property; FILTER is // the same criteria-based selection. - "QSELECT" | "QS" | "FILTER" | "FI" => { + "QSELECT" | "FILTER" => { return Some(Task::done(Message::QSelectOpen)); } @@ -75,7 +75,7 @@ impl OpenCADStudio { } // ── LIST — entity info ──────────────────────────────────────── - "LIST" | "LI" => { + "LIST" => { let selected: Vec<_> = self.tabs[i].scene.selected_entities(); if selected.is_empty() { self.command_line @@ -218,35 +218,35 @@ impl OpenCADStudio { } // ── Break / Join ───────────────────────────────────────────────── - "JOIN" | "J" => { + "JOIN" => { use crate::modules::draw::modify::join::JoinCommand; let cmd = JoinCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "BREAK" | "BR" => { + "BREAK" => { use crate::modules::draw::modify::break_cmd::BreakInteractiveCommand; let cmd = BreakInteractiveCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "BREAKATPOINT" | "BAP" => { + "BREAKATPOINT" => { use crate::modules::draw::modify::break_cmd::BreakAtPointCommand; let cmd = BreakAtPointCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "PEDIT" | "PE" => { + "PEDIT" => { use crate::modules::draw::modify::pedit::PeditCommand; let cmd_obj = PeditCommand::new(); self.command_line.push_info(&cmd_obj.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); } - "SPLINEDIT" | "SPE" => { + "SPLINEDIT" => { use crate::modules::draw::modify::splinedit::SplineditCommand; let cmd_obj = SplineditCommand::new(); self.command_line.push_info(&cmd_obj.prompt()); @@ -257,7 +257,7 @@ impl OpenCADStudio { // If a single block with attributes is already selected it opens on // that block; otherwise the pick command runs and the editor opens // once a block is chosen (see `command_driver`). - "ATTEDIT" | "ATE" => { + "ATTEDIT" => { let selected_attr_insert = { let sel = self.tabs[i].scene.selected_entities(); if sel.len() == 1 { @@ -547,28 +547,28 @@ impl OpenCADStudio { .push_output("REFCLOSE: Changes discarded."); } - "ALIGN" | "AL" => { + "ALIGN" => { use crate::modules::draw::modify::align::AlignCommand; let cmd = AlignCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "LENGTHEN" | "LEN" => { + "LENGTHEN" => { use crate::modules::draw::modify::lengthen::LengthenCommand; let cmd = LengthenCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "DIVIDE" | "DIV" => { + "DIVIDE" => { use crate::modules::draw::inquiry::divide::DivideCommand; let cmd = DivideCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "MEASURE" | "ME" => { + "MEASURE" => { use crate::modules::draw::inquiry::divide::MeasureCommand; let cmd = MeasureCommand::new(); self.command_line.push_info(&cmd.prompt()); @@ -576,7 +576,7 @@ impl OpenCADStudio { } // ── Inquiry ────────────────────────────────────────────────────── - "DIST" | "DI" => { + "DIST" => { use crate::modules::draw::inquiry::dist::DistCommand; let cmd = DistCommand::new(); self.command_line.push_info(&cmd.prompt()); diff --git a/src/app/commands/layers.rs b/src/app/commands/layers.rs index b6d3170eb..cfddbd0b6 100644 --- a/src/app/commands/layers.rs +++ b/src/app/commands/layers.rs @@ -510,7 +510,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "MATCHPROP" | "MA" => { + "MATCHPROP" => { use crate::modules::draw::properties::match_prop::MatchPropCommand; self.tabs[i].scene.deselect_all(); let cmd = MatchPropCommand::new(); @@ -518,7 +518,7 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd)); } - "GROUP" | "G" => { + "GROUP" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() @@ -540,7 +540,7 @@ impl OpenCADStudio { } } - "UNGROUP" | "UG" => { + "UNGROUP" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index c32f95ab3..71e03cb79 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -59,6 +59,12 @@ impl OpenCADStudio { fn dispatch_command_inner(&mut self, cmd: &str, allow_suggest: bool) -> Task { let i = self.active_tab; + // Expand a command alias ("L" → "LINE") on the leading token before any + // routing, so every path below (Start-tab gate, plugins, all dispatch + // families, the Repeat menu) sees the canonical command. Arguments after + // the first space are left untouched. A non-alias passes through as-is. + let resolved = self.resolve_alias(cmd); + let cmd = resolved.as_deref().unwrap_or(cmd); // Starting a command closes any open ribbon dropdown (e.g. a style // combo left open) so it does not stay stuck behind the new tool. self.ribbon.close_dropdown(); @@ -151,6 +157,7 @@ impl OpenCADStudio { if let Some(top) = crate::ui::command_line::ranked_matches( cmd, &self.command_line.dynamic_commands, + &self.command_line.command_aliases, ) .into_iter() .next() diff --git a/src/app/commands/view.rs b/src/app/commands/view.rs index 99890c779..e8e22dc1a 100644 --- a/src/app/commands/view.rs +++ b/src/app/commands/view.rs @@ -208,10 +208,15 @@ impl OpenCADStudio { self.command_line.push_info("Opening release notes..."); } - // ── CUI / ALIASEDIT — customization entry points ─────────────── - // Command-alias and key-binding editing lives in the keyboard - // shortcuts panel, so route the customization verbs there. - "CUI" | "ALIASEDIT" => { + // ── ALIASEDIT — command-alias editor ─────────────────────────── + // Opens the command-alias table editor (ocad.pgp). Keyboard + // key-bindings are edited separately via CUI (below). + "ALIASEDIT" => { + return Some(Task::done(Message::AliasEditorOpen)); + } + + // ── CUI — keyboard shortcut / key-binding editor ─────────────── + "CUI" => { return Some(Task::done(Message::ShortcutsPanelOpen)); } @@ -380,7 +385,7 @@ impl OpenCADStudio { } // ── Layout / viewport ────────────────────────────────────────── - "MVIEW" | "MV" => { + "MVIEW" => { if self.tabs[i].scene.current_layout == "Model" { self.command_line .push_error("MVIEW: switch to a paper space layout first."); @@ -393,7 +398,7 @@ impl OpenCADStudio { } // ── MSPACE / PSPACE ─────────────────────────────────────────── - "MS" | "MSPACE" => { + "MSPACE" => { return Some(Task::done(Message::MspaceCommand)); } "PSPACE" => { @@ -970,7 +975,7 @@ impl OpenCADStudio { } // HIDE — hidden-line view of the active viewport. - "HIDE" | "HI" => { + "HIDE" => { return Some(Task::done(Message::SetRenderMode( acadrust::entities::ViewportRenderMode::HiddenLine, ))); diff --git a/src/app/mod.rs b/src/app/mod.rs index 65674f1d9..614194709 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,3 +1,4 @@ +mod alias; #[cfg(not(target_arch = "wasm32"))] mod automation; #[cfg(not(target_arch = "wasm32"))] @@ -629,6 +630,16 @@ pub(super) struct OpenCADStudio { /// User-defined function-key overrides: "F3" → command string. shortcut_overrides: rustc_hash::FxHashMap, + // ── Command Aliases ─────────────────────────────────────────────────── + /// Command-line aliases: uppercase abbreviation → uppercase command + /// ("L" → "LINE"). Loaded from `ocad.pgp` at boot and consulted before + /// every dispatch (`resolve_alias`); user-editable via ALIASEDIT. See + /// [`alias`]. + command_aliases: rustc_hash::FxHashMap, + /// Working buffer for the ALIASEDIT modal: `(alias, command)` rows being + /// edited. Seeded from `command_aliases` on open, committed back on close. + alias_editor_rows: Vec<(String, String)>, + // ── Layout Manager Panel ────────────────────────────────────────────── layout_manager_selected: String, layout_manager_rename_buf: String, @@ -1081,6 +1092,7 @@ pub enum ModalKind { PointStyle, AttributeEditor, LayerDeleteWarning, + Aliases, } /// Identifies a DimStyle field that can be edited in the dialog. @@ -1628,6 +1640,21 @@ pub enum Message { ShortcutsPanelOpen, #[allow(dead_code)] ShortcutsPanelClose, + // ── Command Alias Editor (ALIASEDIT) ──────────────────────────────── + /// Open the command-alias editor modal, seeding rows from the alias table. + AliasEditorOpen, + /// Live edit of the alias or command in row `idx`. + AliasEditorInput { + idx: usize, + field: crate::ui::window::alias_editor::AliasField, + value: String, + }, + /// Append a blank alias row. + AliasEditorAdd, + /// Remove alias row `idx`. + AliasEditorRemove(usize), + /// Commit the edited rows to the alias table (Apply button); stays open. + AliasEditorApply, // ── About window ──────────────────────────────────────────────────── AboutOpen, /// Close whatever in-canvas modal dialog is open (Plan B). @@ -2187,6 +2214,9 @@ impl OpenCADStudio { active_theme: Theme::Dark, // Keyboard shortcuts shortcut_overrides: rustc_hash::FxHashMap::default(), + // Command aliases (populated from ocad.pgp just after construction) + command_aliases: rustc_hash::FxHashMap::default(), + alias_editor_rows: Vec::new(), // Layout Manager layout_manager_selected: "Model".to_string(), layout_manager_rename_buf: String::new(), @@ -2328,6 +2358,11 @@ impl OpenCADStudio { if let Some(s) = settings::UserSettings::load() { app.apply_settings(&s); } + // Load command aliases from ocad.pgp (writes the shipped default file + // on first launch). The hide-set keeps aliases out of autocomplete while + // their target command still shows. + app.command_aliases = alias::load_aliases(); + app.command_line.command_aliases = app.command_aliases.clone(); // Load external plugin packages from the plugins folder once, then fold // their ribbon tabs into the ribbon. Skipped under test/wasm. #[cfg(all(not(target_arch = "wasm32"), not(test)))] diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index ee5515bbf..1b93da79a 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -118,6 +118,12 @@ impl OpenCADStudio { self.attr_editor_selected = 0; self.attr_editor_tab = crate::ui::window::attribute_editor::AttrTab::Attribute; } + // Closing (✕) discards edits made since the last Apply — matching the + // style editors. Committing happens only through the Apply button. + Some(Aliases) => { + self.alias_editor_rows.clear(); + self.ribbon.deactivate_tool_if("ALIASEDIT"); + } _ => {} } self.active_modal = None; @@ -2631,6 +2637,52 @@ impl OpenCADStudio { Task::none() } + // ── Command Alias Editor (ALIASEDIT) ────────────────────────────── + Message::AliasEditorOpen => { + // Seed the working buffer from the current table, sorted by alias + // so the list is stable and diffable. + let mut rows: Vec<(String, String)> = self + .command_aliases + .iter() + .map(|(a, c)| (a.clone(), c.clone())) + .collect(); + rows.sort_by(|a, b| a.0.cmp(&b.0)); + self.alias_editor_rows = rows; + self.active_modal = Some(super::ModalKind::Aliases); + Task::none() + } + Message::AliasEditorInput { idx, field, value } => { + use crate::ui::window::alias_editor::AliasField; + // Aliases and commands are stored uppercase; uppercasing as the + // user types keeps display and the committed table consistent. + let value = value.to_uppercase(); + if let Some(rowdata) = self.alias_editor_rows.get_mut(idx) { + match field { + AliasField::Alias => rowdata.0 = value, + AliasField::Command => rowdata.1 = value, + } + } + Task::none() + } + Message::AliasEditorAdd => { + self.alias_editor_rows.push((String::new(), String::new())); + Task::none() + } + Message::AliasEditorRemove(idx) => { + if idx < self.alias_editor_rows.len() { + self.alias_editor_rows.remove(idx); + } + Task::none() + } + Message::AliasEditorApply => { + self.apply_alias_editor_rows(); + self.command_line.push_info(&format!( + "{} alias(es) applied.", + self.command_aliases.len() + )); + Task::none() + } + // ── About window ────────────────────────────────────────────── Message::AboutOpen => { self.active_modal = Some(super::ModalKind::About); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 04b182fdb..6416f8986 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1495,6 +1495,7 @@ impl OpenCADStudio { PointStyle => (360, 470), AttributeEditor => (640, 500), LayerDeleteWarning => (440, 200), + Aliases => (480, 520), }; Some((w as f32 + EXTRA_W, h as f32 + EXTRA_H)) } diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 69c71e768..30b378253 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -21,6 +21,9 @@ impl OpenCADStudio { super::super::ModalKind::Shortcuts => { sized(crate::ui::window::shortcuts::view_window(&self.shortcut_overrides), 720, 520) } + super::super::ModalKind::Aliases => { + sized(crate::ui::window::alias_editor::view_window(&self.alias_editor_rows), 480, 520) + } super::super::ModalKind::PluginManager => sized( crate::ui::window::plugin_manager::view_window( &self.disabled_plugins, diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index b9bd5e57b..1cee532e0 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -257,4 +257,4 @@ fn preview_aligned(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> WireModel { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DAL", "DIMALIGNED"] }); // AlignedDimensionCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMALIGNED"] }); // AlignedDimensionCommand diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 04534e994..fc1ba7678 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -179,4 +179,4 @@ fn angular_preview(vertex: Vec3, first: Vec3, second: Vec3, arc_pt: Vec3) -> Vec // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DIMANGULAR", "DAN"] }); // AngularDimensionCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMANGULAR"] }); // AngularDimensionCommand diff --git a/src/modules/annotate/ddedit.rs b/src/modules/annotate/ddedit.rs index 318258431..3c44b5c61 100644 --- a/src/modules/annotate/ddedit.rs +++ b/src/modules/annotate/ddedit.rs @@ -66,4 +66,4 @@ impl CadCommand for DdeditCommand { } // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DDEDIT", "ED"] }); +inventory::submit!(crate::command::CommandRegistration { names: &["DDEDIT"] }); diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index ba66c17a8..cb1f4995e 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -202,4 +202,4 @@ fn preview_line(a: Vec3, b: Vec3) -> WireModel { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DDI", "DIMDIAMETER"] }); // DiameterDimensionCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMDIAMETER"] }); // DiameterDimensionCommand diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index 3850d2c80..32e71734e 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -206,4 +206,4 @@ fn v3(p: Vec3) -> Vector3 { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DBA", "DIMBASELINE"] }); // DimBaselineCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMBASELINE"] }); // DimBaselineCommand diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index 877a0d0e1..0914b6ced 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -186,4 +186,4 @@ fn v3(p: Vec3) -> Vector3 { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DCO", "DIMCONTINUE"] }); // DimContinueCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] }); // DimContinueCommand diff --git a/src/modules/annotate/dimbreak.rs b/src/modules/annotate/dimbreak.rs index 762476091..49aac5cfd 100644 --- a/src/modules/annotate/dimbreak.rs +++ b/src/modules/annotate/dimbreak.rs @@ -100,4 +100,4 @@ impl CadCommand for DimBreakCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DBR", "DIMBREAK"] }); // DimBreakCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMBREAK"] }); // DimBreakCommand diff --git a/src/modules/annotate/dimedit.rs b/src/modules/annotate/dimedit.rs index 1be730f69..8a9cb80c0 100644 --- a/src/modules/annotate/dimedit.rs +++ b/src/modules/annotate/dimedit.rs @@ -88,4 +88,4 @@ impl CadCommand for DimEditCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DED", "DIMEDIT"] }); // DimEditCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMEDIT"] }); // DimEditCommand diff --git a/src/modules/annotate/dimjogline.rs b/src/modules/annotate/dimjogline.rs index c10f61937..dd6a77957 100644 --- a/src/modules/annotate/dimjogline.rs +++ b/src/modules/annotate/dimjogline.rs @@ -117,4 +117,4 @@ impl CadCommand for DimJogLineCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DIMJOGLINE", "DJL"] }); // DimJogLineCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMJOGLINE"] }); // DimJogLineCommand diff --git a/src/modules/annotate/leader_cmd.rs b/src/modules/annotate/leader_cmd.rs index bc67ca596..74160a1b7 100644 --- a/src/modules/annotate/leader_cmd.rs +++ b/src/modules/annotate/leader_cmd.rs @@ -206,4 +206,4 @@ pub fn arrowhead_wings(tip: Vec3, next: Vec3, size: f32) -> [Vec3; 2] { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["LE", "LEADER"] }); // LeaderCommand +inventory::submit!(crate::command::CommandRegistration { names: &["LEADER"] }); // LeaderCommand diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index 0593adaeb..a67aa4ff6 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -266,4 +266,4 @@ fn linear_text_pos(first: Vec3, second: Vec3, def: Vec3, axis: Vec3) -> Vec3 { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DIMLINEAR", "DLI"] }); // LinearDimensionCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMLINEAR"] }); // LinearDimensionCommand diff --git a/src/modules/annotate/mleader_cmd.rs b/src/modules/annotate/mleader_cmd.rs index 5bb130e93..a3ffcace7 100644 --- a/src/modules/annotate/mleader_cmd.rs +++ b/src/modules/annotate/mleader_cmd.rs @@ -192,4 +192,4 @@ fn arrowhead_wings(tip: Vec3, next: Vec3, size: f32) -> [Vec3; 2] { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["MLD", "MLEADER"] }); // MLeaderCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MLEADER"] }); // MLeaderCommand diff --git a/src/modules/annotate/mleader_edit.rs b/src/modules/annotate/mleader_edit.rs index 2bf142885..3034109f2 100644 --- a/src/modules/annotate/mleader_edit.rs +++ b/src/modules/annotate/mleader_edit.rs @@ -482,7 +482,7 @@ fn _uses_ml_types(_ml: &MultiLeader, _ll: &LeaderLine) {} // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["MLA", "MLEADERADD"] }); // MLeaderAddCommand -inventory::submit!(crate::command::CommandRegistration { names: &["MLAL", "MLEADERALIGN"] }); // MLeaderAlignCommand -inventory::submit!(crate::command::CommandRegistration { names: &["MLC", "MLEADERCOLLECT"] }); // MLeaderCollectCommand -inventory::submit!(crate::command::CommandRegistration { names: &["MLEADERREMOVE", "MLR"] }); // MLeaderRemoveCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MLEADERADD"] }); // MLeaderAddCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MLEADERALIGN"] }); // MLeaderAlignCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MLEADERCOLLECT"] }); // MLeaderCollectCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MLEADERREMOVE"] }); // MLeaderRemoveCommand diff --git a/src/modules/annotate/mtext.rs b/src/modules/annotate/mtext.rs index c55d91df8..14c312b68 100644 --- a/src/modules/annotate/mtext.rs +++ b/src/modules/annotate/mtext.rs @@ -65,4 +65,4 @@ impl CadCommand for MTextCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["MT", "MTEXT"] }); // MTextCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MTEXT"] }); // MTextCommand diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index fa69d1105..9ac937850 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -139,4 +139,4 @@ fn preview_wire(points: Vec) -> WireModel { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DIMORDINATE", "DOR"] }); // OrdinateDimCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMORDINATE"] }); // OrdinateDimCommand diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index c86153bae..25ba8efe1 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -207,4 +207,4 @@ fn preview_wire(points: Vec) -> WireModel { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DIMRADIUS", "DRA"] }); // RadiusDimensionCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMRADIUS"] }); // RadiusDimensionCommand diff --git a/src/modules/annotate/text.rs b/src/modules/annotate/text.rs index 1f6544078..532117323 100644 --- a/src/modules/annotate/text.rs +++ b/src/modules/annotate/text.rs @@ -65,4 +65,4 @@ impl CadCommand for TextCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DT", "T", "TEXT"] }); // TextCommand +inventory::submit!(crate::command::CommandRegistration { names: &["TEXT"] }); // TextCommand diff --git a/src/modules/annotate/tolerance_cmd.rs b/src/modules/annotate/tolerance_cmd.rs index 5c7b001f2..280f848b8 100644 --- a/src/modules/annotate/tolerance_cmd.rs +++ b/src/modules/annotate/tolerance_cmd.rs @@ -114,4 +114,4 @@ impl CadCommand for ToleranceCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["TOL", "TOLERANCE"] }); // ToleranceCommand +inventory::submit!(crate::command::CommandRegistration { names: &["TOLERANCE"] }); // ToleranceCommand diff --git a/src/modules/draw/clipboard/paste.rs b/src/modules/draw/clipboard/paste.rs index c5a6ce976..44c445b09 100644 --- a/src/modules/draw/clipboard/paste.rs +++ b/src/modules/draw/clipboard/paste.rs @@ -131,6 +131,6 @@ impl CadCommand for PasteCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["PASTECLIP", "PC"] }); // PasteCommand +inventory::submit!(crate::command::CommandRegistration { names: &["PASTECLIP"] }); // PasteCommand inventory::submit!(crate::command::CommandRegistration { names: &["PASTEORIG"] }); inventory::submit!(crate::command::CommandRegistration { names: &["PASTEBLOCK"] }); diff --git a/src/modules/draw/draw/arc.rs b/src/modules/draw/draw/arc.rs index 80b5abfcb..56b7f6c6e 100644 --- a/src/modules/draw/draw/arc.rs +++ b/src/modules/draw/draw/arc.rs @@ -1392,7 +1392,7 @@ impl CadCommand for ArcContCommand { inventory::submit!(crate::command::CommandRegistration { names: &["ARC_3P"] }); // Arc3PCommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSA"] }); // ArcCSACommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSL"] }); // ArcCSLCommand -inventory::submit!(crate::command::CommandRegistration { names: &["A", "ARC"] }); // ArcCommand +inventory::submit!(crate::command::CommandRegistration { names: &["ARC"] }); // ArcCommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCA"] }); // ArcSCACommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCE"] }); // ArcSCECommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCL"] }); // ArcSCLCommand diff --git a/src/modules/draw/draw/circle.rs b/src/modules/draw/draw/circle.rs index 47d598ad3..c7b4faea5 100644 --- a/src/modules/draw/draw/circle.rs +++ b/src/modules/draw/draw/circle.rs @@ -991,6 +991,6 @@ impl CadCommand for CircleTTTCommand { inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_2P"] }); // Circle2PCommand inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_3P"] }); // Circle3PCommand inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_CD"] }); // CircleCDCommand -inventory::submit!(crate::command::CommandRegistration { names: &["C", "CIRCLE"] }); // CircleCommand +inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE"] }); // CircleCommand inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTR"] }); // CircleTTRCommand inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTT"] }); // CircleTTTCommand diff --git a/src/modules/draw/draw/donut.rs b/src/modules/draw/draw/donut.rs index 377686e11..e75582a79 100644 --- a/src/modules/draw/draw/donut.rs +++ b/src/modules/draw/draw/donut.rs @@ -143,4 +143,4 @@ fn make_donut(cx: f64, cy: f64, inner_r: f64, outer_r: f64) -> EntityType { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DO", "DONUT"] }); // DonutCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DONUT"] }); // DonutCommand diff --git a/src/modules/draw/draw/ellipse.rs b/src/modules/draw/draw/ellipse.rs index f782b2289..45975855e 100644 --- a/src/modules/draw/draw/ellipse.rs +++ b/src/modules/draw/draw/ellipse.rs @@ -646,4 +646,4 @@ fn line_wire(from: DVec3, to: DVec3) -> WireModel { // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["ELLIPSE_ARC"] }); // EllipseArcCommand inventory::submit!(crate::command::CommandRegistration { names: &["ELLIPSE_AXIS"] }); // EllipseAxisCommand -inventory::submit!(crate::command::CommandRegistration { names: &["EL", "ELLIPSE"] }); // EllipseCommand +inventory::submit!(crate::command::CommandRegistration { names: &["ELLIPSE"] }); // EllipseCommand diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index dce2af062..db010c9e3 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -455,4 +455,4 @@ impl CadCommand for BoundaryCommand { // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["BOUNDARY"] }); // BoundaryCommand inventory::submit!(crate::command::CommandRegistration { names: &["GRADIENT"] }); // GradientCommand -inventory::submit!(crate::command::CommandRegistration { names: &["H", "HATCH"] }); // HatchCommand +inventory::submit!(crate::command::CommandRegistration { names: &["HATCH"] }); // HatchCommand diff --git a/src/modules/draw/draw/hatchedit.rs b/src/modules/draw/draw/hatchedit.rs index 7620df141..1f0cd4d97 100644 --- a/src/modules/draw/draw/hatchedit.rs +++ b/src/modules/draw/draw/hatchedit.rs @@ -164,4 +164,4 @@ impl CadCommand for HatcheditCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["HATCHEDIT", "HE"] }); // HatcheditCommand +inventory::submit!(crate::command::CommandRegistration { names: &["HATCHEDIT"] }); // HatcheditCommand diff --git a/src/modules/draw/draw/line.rs b/src/modules/draw/draw/line.rs index b0fd98b00..c74cac6ee 100644 --- a/src/modules/draw/draw/line.rs +++ b/src/modules/draw/draw/line.rs @@ -331,4 +331,4 @@ mod tangent_tests { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["L", "LINE"] }); // LineCommand +inventory::submit!(crate::command::CommandRegistration { names: &["LINE"] }); // LineCommand diff --git a/src/modules/draw/draw/mline.rs b/src/modules/draw/draw/mline.rs index 3757d8491..2a114021f 100644 --- a/src/modules/draw/draw/mline.rs +++ b/src/modules/draw/draw/mline.rs @@ -170,4 +170,4 @@ fn build_mline(pts: &[Vec3], scale: f64, closed: bool, style_name: &str) -> Enti // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["ML", "MLINE"] }); // MlineCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MLINE"] }); // MlineCommand diff --git a/src/modules/draw/draw/point.rs b/src/modules/draw/draw/point.rs index 56fb4978f..4b6ff8b0e 100644 --- a/src/modules/draw/draw/point.rs +++ b/src/modules/draw/draw/point.rs @@ -63,7 +63,7 @@ impl CadCommand for PointCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["PO", "POINT"] }); // PointCommand +inventory::submit!(crate::command::CommandRegistration { names: &["POINT"] }); // PointCommand // Point display style system variables + dialog. inventory::submit!(crate::command::CommandRegistration { names: &["PDMODE", "PDSIZE", "DDPTYPE"] diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index 009b119e4..32a5557a5 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -351,4 +351,4 @@ impl CadCommand for PlineCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["PL", "PLINE"] }); // PlineCommand +inventory::submit!(crate::command::CommandRegistration { names: &["PLINE"] }); // PlineCommand diff --git a/src/modules/draw/draw/ray.rs b/src/modules/draw/draw/ray.rs index 3e62cc65d..213d34b43 100644 --- a/src/modules/draw/draw/ray.rs +++ b/src/modules/draw/draw/ray.rs @@ -201,4 +201,4 @@ impl CadCommand for XLineCommand { // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["RAY"] }); // RayCommand -inventory::submit!(crate::command::CommandRegistration { names: &["CONSTRUCTIONLINE", "XL", "XLINE"] }); // XLineCommand +inventory::submit!(crate::command::CommandRegistration { names: &["CONSTRUCTIONLINE", "XLINE"] }); // XLineCommand diff --git a/src/modules/draw/draw/shapes.rs b/src/modules/draw/draw/shapes.rs index 733710d36..483a4fc8d 100644 --- a/src/modules/draw/draw/shapes.rs +++ b/src/modules/draw/draw/shapes.rs @@ -834,8 +834,8 @@ fn edge_poly_params(a: DVec3, b: DVec3, sides: u32) -> Option<(DVec3, f64, f64)> // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["POLY_C"] }); // PolyCCommand -inventory::submit!(crate::command::CommandRegistration { names: &["POL", "POLY", "POLYGON"] }); // PolyCommand +inventory::submit!(crate::command::CommandRegistration { names: &["POLY", "POLYGON"] }); // PolyCommand inventory::submit!(crate::command::CommandRegistration { names: &["POLY_E"] }); // PolyECommand inventory::submit!(crate::command::CommandRegistration { names: &["RECT_CEN"] }); // RectCenCommand -inventory::submit!(crate::command::CommandRegistration { names: &["REC", "RECT", "RECTANG"] }); // RectCommand +inventory::submit!(crate::command::CommandRegistration { names: &["RECT", "RECTANG"] }); // RectCommand inventory::submit!(crate::command::CommandRegistration { names: &["RECT_ROT"] }); // RectRotCommand diff --git a/src/modules/draw/draw/spline.rs b/src/modules/draw/draw/spline.rs index 3bfcb0d57..7390cb51c 100644 --- a/src/modules/draw/draw/spline.rs +++ b/src/modules/draw/draw/spline.rs @@ -190,4 +190,4 @@ impl CadCommand for SplineCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["SPL", "SPLINE"] }); // SplineCommand +inventory::submit!(crate::command::CommandRegistration { names: &["SPLINE"] }); // SplineCommand diff --git a/src/modules/draw/draw/wipeout.rs b/src/modules/draw/draw/wipeout.rs index 0adafd991..9fbc5eace 100644 --- a/src/modules/draw/draw/wipeout.rs +++ b/src/modules/draw/draw/wipeout.rs @@ -191,4 +191,4 @@ fn make_poly_wipeout(pts: &[Vec3]) -> EntityType { } // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["WIPEOUT", "WO"] }); +inventory::submit!(crate::command::CommandRegistration { names: &["WIPEOUT"] }); diff --git a/src/modules/draw/groups/ungroup.rs b/src/modules/draw/groups/ungroup.rs index 606f7f4b8..73fd352a0 100644 --- a/src/modules/draw/groups/ungroup.rs +++ b/src/modules/draw/groups/ungroup.rs @@ -62,4 +62,4 @@ impl CadCommand for UngroupCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["UG", "UNGROUP"] }); // UngroupCommand +inventory::submit!(crate::command::CommandRegistration { names: &["UNGROUP"] }); // UngroupCommand diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index da4d5e705..7fa2faeb4 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -90,4 +90,4 @@ impl CadCommand for DistCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DI", "DIST"] }); // DistCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIST"] }); // DistCommand diff --git a/src/modules/draw/inquiry/divide.rs b/src/modules/draw/inquiry/divide.rs index 81f280f49..9db7ea364 100644 --- a/src/modules/draw/inquiry/divide.rs +++ b/src/modules/draw/inquiry/divide.rs @@ -314,5 +314,5 @@ fn arc_span_rad(start: f64, end: f64) -> f64 { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DIV", "DIVIDE"] }); // DivideCommand -inventory::submit!(crate::command::CommandRegistration { names: &["ME", "MEASURE"] }); // MeasureCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIVIDE"] }); // DivideCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MEASURE"] }); // MeasureCommand diff --git a/src/modules/draw/modify/align.rs b/src/modules/draw/modify/align.rs index 8d44f4191..f94befb1f 100644 --- a/src/modules/draw/modify/align.rs +++ b/src/modules/draw/modify/align.rs @@ -192,4 +192,4 @@ impl AlignCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["AL", "ALIGN"] }); // AlignCommand +inventory::submit!(crate::command::CommandRegistration { names: &["ALIGN"] }); // AlignCommand diff --git a/src/modules/draw/modify/attedit.rs b/src/modules/draw/modify/attedit.rs index 1e49ce407..03338b447 100644 --- a/src/modules/draw/modify/attedit.rs +++ b/src/modules/draw/modify/attedit.rs @@ -62,4 +62,4 @@ impl CadCommand for AtteditCommand { } // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["ATTEDIT", "ATE"] }); +inventory::submit!(crate::command::CommandRegistration { names: &["ATTEDIT"] }); diff --git a/src/modules/draw/modify/break_cmd.rs b/src/modules/draw/modify/break_cmd.rs index 1aacdb156..ccb57444b 100644 --- a/src/modules/draw/modify/break_cmd.rs +++ b/src/modules/draw/modify/break_cmd.rs @@ -470,5 +470,5 @@ impl CadCommand for BreakAtPointCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["BAP", "BREAKATPOINT"] }); // BreakAtPointCommand -inventory::submit!(crate::command::CommandRegistration { names: &["BR", "BREAK"] }); // BreakInteractiveCommand +inventory::submit!(crate::command::CommandRegistration { names: &["BREAKATPOINT"] }); // BreakAtPointCommand +inventory::submit!(crate::command::CommandRegistration { names: &["BREAK"] }); // BreakInteractiveCommand diff --git a/src/modules/draw/modify/fillet.rs b/src/modules/draw/modify/fillet.rs index 116c8ae81..8567aa615 100644 --- a/src/modules/draw/modify/fillet.rs +++ b/src/modules/draw/modify/fillet.rs @@ -1950,5 +1950,5 @@ impl CadCommand for ChamferCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["CHA", "CHAMFER"] }); // ChamferCommand -inventory::submit!(crate::command::CommandRegistration { names: &["F", "FILLET"] }); // FilletCommand +inventory::submit!(crate::command::CommandRegistration { names: &["CHAMFER"] }); // ChamferCommand +inventory::submit!(crate::command::CommandRegistration { names: &["FILLET"] }); // FilletCommand diff --git a/src/modules/draw/modify/join.rs b/src/modules/draw/modify/join.rs index 70c60b3ab..58bd10ade 100644 --- a/src/modules/draw/modify/join.rs +++ b/src/modules/draw/modify/join.rs @@ -264,4 +264,4 @@ fn is_collinear(verts: &[(DVec3, f64)]) -> bool { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["J", "JOIN"] }); // JoinCommand +inventory::submit!(crate::command::CommandRegistration { names: &["JOIN"] }); // JoinCommand diff --git a/src/modules/draw/modify/lengthen.rs b/src/modules/draw/modify/lengthen.rs index c5527dc38..971f9e63c 100644 --- a/src/modules/draw/modify/lengthen.rs +++ b/src/modules/draw/modify/lengthen.rs @@ -512,4 +512,4 @@ fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option bool { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["PE", "PEDIT"] }); // PeditCommand +inventory::submit!(crate::command::CommandRegistration { names: &["PEDIT"] }); // PeditCommand diff --git a/src/modules/draw/modify/splinedit.rs b/src/modules/draw/modify/splinedit.rs index 83326eef6..85d675a58 100644 --- a/src/modules/draw/modify/splinedit.rs +++ b/src/modules/draw/modify/splinedit.rs @@ -203,4 +203,4 @@ pub fn apply_spline_op(doc: &mut acadrust::CadDocument, handle: acadrust::Handle // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["SPE", "SPLINEDIT"] }); // SplineditCommand +inventory::submit!(crate::command::CommandRegistration { names: &["SPLINEDIT"] }); // SplineditCommand diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index bd6cddfd8..7bd065978 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -2751,8 +2751,8 @@ impl CadCommand for ExtendCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["EX", "EXTEND"] }); // ExtendCommand -inventory::submit!(crate::command::CommandRegistration { names: &["TR", "TRIM"] }); // TrimCommand +inventory::submit!(crate::command::CommandRegistration { names: &["EXTEND"] }); // ExtendCommand +inventory::submit!(crate::command::CommandRegistration { names: &["TRIM"] }); // TrimCommand #[cfg(test)] mod tests { diff --git a/src/modules/draw/properties/match_prop.rs b/src/modules/draw/properties/match_prop.rs index d08f34bd2..50af4005e 100644 --- a/src/modules/draw/properties/match_prop.rs +++ b/src/modules/draw/properties/match_prop.rs @@ -92,4 +92,4 @@ impl CadCommand for MatchPropCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["MA", "MATCHPROP"] }); // MatchPropCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MATCHPROP"] }); // MatchPropCommand diff --git a/src/modules/draw/select.rs b/src/modules/draw/select.rs index d198188ce..8bf36d465 100644 --- a/src/modules/draw/select.rs +++ b/src/modules/draw/select.rs @@ -108,4 +108,4 @@ impl CadCommand for SelectObjectsCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["AR", "ARRAY", "ARRAYPATH", "ARRAYPOLAR", "ARRAYRECT", "BLOCK", "CC", "CO", "COPY", "COPYCLIP", "CUTCLIP", "CX", "E", "ERASE", "EXPLODE", "G", "GROUP", "LAYFRZ", "LAYLCK", "LAYMCUR", "LAYOFF", "LAYULK", "M", "MI", "MIRROR", "MOVE", "RO", "ROTATE", "SC", "SCALE", "SS", "STRETCH", "X"] }); // SelectObjectsCommand +inventory::submit!(crate::command::CommandRegistration { names: &["ARRAY", "ARRAYPATH", "ARRAYPOLAR", "ARRAYRECT", "BLOCK", "COPY", "COPYCLIP", "CUTCLIP", "ERASE", "EXPLODE", "GROUP", "LAYFRZ", "LAYLCK", "LAYMCUR", "LAYOFF", "LAYULK", "MIRROR", "MOVE", "ROTATE", "SCALE", "STRETCH"] }); // SelectObjectsCommand diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 9d4f1418f..b1a816104 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -329,7 +329,7 @@ pub fn empty_solid3d() -> EntityType { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["EXT", "EXTRUDE"] }); // ExtrudeCommand +inventory::submit!(crate::command::CommandRegistration { names: &["EXTRUDE"] }); // ExtrudeCommand inventory::submit!(crate::command::CommandRegistration { names: &["LOFT"] }); // LoftCommand -inventory::submit!(crate::command::CommandRegistration { names: &["REV", "REVOLVE"] }); // RevolveCommand +inventory::submit!(crate::command::CommandRegistration { names: &["REVOLVE"] }); // RevolveCommand inventory::submit!(crate::command::CommandRegistration { names: &["SWEEP"] }); // SweepCommand diff --git a/src/modules/layout/mview.rs b/src/modules/layout/mview.rs index 15680d7fe..b5b0f805c 100644 --- a/src/modules/layout/mview.rs +++ b/src/modules/layout/mview.rs @@ -110,4 +110,4 @@ impl CadCommand for MviewCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["MV", "MVIEW"] }); // MviewCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MVIEW"] }); // MviewCommand diff --git a/src/modules/view/plot_window.rs b/src/modules/view/plot_window.rs index ed3ca960c..2a6c90f33 100644 --- a/src/modules/view/plot_window.rs +++ b/src/modules/view/plot_window.rs @@ -81,4 +81,4 @@ impl CadCommand for PlotWindowCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["PLOTWINDOW", "PW"] }); // PlotWindowCommand +inventory::submit!(crate::command::CommandRegistration { names: &["PLOTWINDOW"] }); // PlotWindowCommand diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index 2bf88d5cc..19f7770c1 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -50,6 +50,12 @@ pub struct CommandLine { /// compile-time command registry, so runtime plugin commands are typeable /// and discoverable — not only reachable via ribbon buttons (#272). pub dynamic_commands: Vec, + /// Command aliases (uppercase `alias → command`), a copy of the app's alias + /// table kept here for autocomplete. Aliases are hidden from suggestions + /// (a terse `CC` never appears) while their target command (`COPYCLIP`) + /// still does; and when the whole input is an alias, its target is surfaced + /// as the top suggestion so the highlight matches what Enter runs. (#288) + pub command_aliases: rustc_hash::FxHashMap, /// The active command step's prompt, mirrored here so a step change /// can be detected and the pinned (non-fading) history line updated. step_prompt: Option, @@ -302,7 +308,11 @@ impl CommandLine { /// Autocomplete suggestions for the current input — see /// [`ranked_matches`]. Includes loaded plugins' commands (#272). pub fn autocomplete_matches(&self) -> Vec { - ranked_matches(self.input.trim(), &self.dynamic_commands) + ranked_matches( + self.input.trim(), + &self.dynamic_commands, + &self.command_aliases, + ) } pub fn view<'a>( @@ -625,7 +635,18 @@ impl CommandLine { /// compile-time `inventory` registry — merged with `dynamic`, the command names /// contributed by loaded plugins (runtime, so they can't be `&'static`; see /// #272). Returns owned strings to carry both sources. -pub fn ranked_matches(needle: &str, dynamic: &[String]) -> Vec { +/// +/// `aliases` is the `alias → command` table. Aliases themselves are dropped from +/// the results, so a terse `CC` is typeable and dispatches (via the alias table) +/// but never clutters the suggestions — only its target `COPYCLIP` is offered. +/// And when the whole input is an alias, its target command is forced to the top +/// so the highlighted suggestion matches what pressing Enter actually runs +/// (typing `AA` highlights `AREA`, `L` highlights `LINE`). (#288) +pub fn ranked_matches( + needle: &str, + dynamic: &[String], + aliases: &rustc_hash::FxHashMap, +) -> Vec { let needle = needle.trim().to_uppercase(); if needle.is_empty() { return Vec::new(); @@ -636,7 +657,8 @@ pub fn ranked_matches(needle: &str, dynamic: &[String]) -> Vec { // Plugin names are uppercased to match the built-ins and the needle, // so ranking and display stay consistent across both sources. .chain(dynamic.iter().map(|cmd| cmd.to_uppercase())) - .filter(|cmd| cmd.contains(&needle)) + // Hide aliases (keys of the table); their target command still shows. + .filter(|cmd| cmd.contains(&needle) && !aliases.contains_key(cmd)) .collect(); matches.sort(); matches.dedup(); @@ -647,6 +669,13 @@ pub fn ranked_matches(needle: &str, dynamic: &[String]) -> Vec { .cmp(&!b.starts_with(&needle)) .then_with(|| a.cmp(b)) }); + // If the entire input is an alias, its target is what Enter runs, so make it + // the first (highlighted) suggestion — even when the target has no substring + // overlap with the alias (`AA` → `AREA`) and so wouldn't otherwise appear. + if let Some(target) = aliases.get(&needle) { + matches.retain(|m| m != target); + matches.insert(0, target.clone()); + } matches.truncate(AUTOCOMPLETE_LIMIT); matches } @@ -740,24 +769,54 @@ const INFO_COLOR: Color = Color { #[cfg(test)] mod tests { use super::ranked_matches; + use rustc_hash::FxHashMap; + + fn aliases(pairs: &[(&str, &str)]) -> FxHashMap { + pairs + .iter() + .map(|(a, c)| (a.to_string(), c.to_string())) + .collect() + } #[test] fn dynamic_plugin_commands_surface_in_autocomplete() { + let none = FxHashMap::default(); // No built-in command contains the plugin prefix, so an empty pool // yields nothing — reproducing the #272 bug's blind autocomplete. - assert!(ranked_matches("LS_", &[]).is_empty()); + assert!(ranked_matches("LS_", &[], &none).is_empty()); // A loaded plugin's commands become typeable, case-insensitively, and // the prefix substring surfaces every one of them. let pool = vec!["LS_LABEL".to_string(), "ls_autolabel".to_string()]; - let m = ranked_matches("LS_", &pool); + let m = ranked_matches("LS_", &pool, &none); assert!(m.contains(&"LS_LABEL".to_string()), "got {m:?}"); assert!(m.contains(&"LS_AUTOLABEL".to_string()), "got {m:?}"); } #[test] fn builtin_commands_still_match_with_an_empty_pool() { - let m = ranked_matches("LINE", &[]); + let m = ranked_matches("LINE", &[], &FxHashMap::default()); assert!(m.iter().any(|c| c == "LINE"), "got {m:?}"); } + + #[test] + fn aliases_are_hidden_but_their_command_survives() { + // The alias `L` is dropped from suggestions while `LINE` (its target) + // still appears. (#288) + let a = aliases(&[("L", "LINE")]); + let m = ranked_matches("L", &[], &a); + assert!(!m.iter().any(|c| c == "L"), "alias L should be hidden: {m:?}"); + assert!(m.iter().any(|c| c == "LINE"), "LINE should remain: {m:?}"); + } + + #[test] + fn exact_alias_forces_its_target_to_the_top() { + // Typing a full alias highlights the command Enter will run, even when + // the target shares no substring with the alias (`AA` → `AREA`). (#288) + let a = aliases(&[("AA", "AREA"), ("L", "LINE")]); + let m = ranked_matches("AA", &[], &a); + assert_eq!(m.first().map(String::as_str), Some("AREA"), "got {m:?}"); + let m = ranked_matches("L", &[], &a); + assert_eq!(m.first().map(String::as_str), Some("LINE"), "got {m:?}"); + } } diff --git a/src/ui/window/alias_editor.rs b/src/ui/window/alias_editor.rs new file mode 100644 index 000000000..6309beceb --- /dev/null +++ b/src/ui/window/alias_editor.rs @@ -0,0 +1,157 @@ +//! Command-alias editor — an in-canvas modal (Plan B) for adding, remapping and +//! removing command-line aliases (the `ocad.pgp` table). Opened by ALIASEDIT. +//! Rows are `(alias, command)`; edits are buffered in `alias_editor_rows` and +//! committed to the alias table when the dialog closes. Mirrors the editable-row +//! pattern of the attribute editor and plugin manager. + +use crate::app::Message; +use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; +use iced::{Background, Border, Color, Element, Fill, Length, Theme}; + +/// Which column of an alias row a text edit targets. +#[derive(Clone, Copy, Debug)] +pub enum AliasField { + Alias, + Command, +} + +/// Right-hand lane reserved for the scrollbar so it never overlaps the ✕ column. +const GUTTER: f32 = 16.0; + +const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 }; +const FIELD_BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 }; +const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }; +const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }; +const WHITE: Color = Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0 }; +const ADD_C: Color = Color { r: 0.20, g: 0.40, b: 0.62, a: 1.0 }; + +fn field_style(_: &Theme, _s: text_input::Status) -> text_input::Style { + text_input::Style { + background: Background::Color(FIELD_BG), + border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + icon: WHITE, + placeholder: DIM, + value: WHITE, + selection: Color { r: 0.25, g: 0.40, b: 0.60, a: 1.0 }, + } +} + +/// Build the alias editor content. `rows` is the live working buffer. +pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> { + let title = text("Command Aliases").size(15).color(WHITE); + let hint = text( + "Type an alias and the command it runs (e.g. L → LINE). \ + Apply to save to ocad.pgp; closing discards unapplied edits.", + ) + .size(11) + .color(DIM); + + // Right gutter reserved so the scrollbar has its own lane and never sits on + // top of the row delete (✕) buttons. Applied to both the header and the + // scrollable rows so the columns stay aligned. + let gutter = iced::Padding { top: 0.0, right: GUTTER, bottom: 0.0, left: 0.0 }; + + let head = container( + row![ + container(text("Alias").size(11).color(DIM)).width(Length::Fixed(120.0)), + container(text("Command").size(11).color(DIM)).width(Fill), + Space::new().width(Length::Fixed(30.0)), + ] + .spacing(8), + ) + .padding(gutter); + + let mut list = column![].spacing(3); + for (idx, (alias, cmd)) in rows.iter().enumerate() { + let alias_box = text_input("alias", alias) + .on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Alias, value: v }) + .style(field_style) + .size(13) + .padding([3, 6]) + .width(Length::Fixed(120.0)); + let cmd_box = text_input("command", cmd) + .on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Command, value: v }) + .style(field_style) + .size(13) + .padding([3, 6]) + .width(Fill); + let del = button(crate::ui::icons::tinted(crate::ui::icons::CLOSE, 12.0, WHITE)) + .on_press(Message::AliasEditorRemove(idx)) + .padding([2, 6]) + .style(|_: &Theme, status| { + let bg = if matches!(status, button::Status::Hovered) { + Color { r: 0.45, g: 0.22, b: 0.22, a: 1.0 } + } else { + Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 } + }; + button::Style { + background: Some(Background::Color(bg)), + border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + ..Default::default() + } + }); + list = list.push( + row![alias_box, cmd_box, del] + .spacing(8) + .align_y(iced::Center), + ); + } + + let add = button(text("+ Add alias").size(12).color(WHITE)) + .on_press(Message::AliasEditorAdd) + .padding([4, 10]) + .style(|_: &Theme, status| { + let bg = if matches!(status, button::Status::Hovered) { + Color { r: 0.30, g: 0.30, b: 0.30, a: 1.0 } + } else { + Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 } + }; + button::Style { + background: Some(Background::Color(bg)), + text_color: WHITE, + border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + ..Default::default() + } + }); + + // Apply — primary action; commits the rows to ocad.pgp and stays open. + let apply = button(text("Apply").size(12).color(WHITE)) + .on_press(Message::AliasEditorApply) + .padding([4, 16]) + .style(|_: &Theme, status| { + let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) { + Color { r: 0.24, g: 0.46, b: 0.74, a: 1.0 } + } else { + ADD_C + }; + button::Style { + background: Some(Background::Color(bg)), + text_color: WHITE, + border: Border { radius: 4.0.into(), ..Default::default() }, + ..Default::default() + } + }); + + container( + column![ + title, + hint, + Space::new().height(6), + head, + scrollable(container(list).padding(gutter)).height(Fill), + Space::new().height(6), + row![add, Space::new().width(Fill), apply].align_y(iced::Center), + ] + .spacing(6) + .width(Fill) + .height(Fill), + ) + .padding(12) + .width(Fill) + .height(Fill) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(BG)), + ..Default::default() + }) + .into() +} diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index f09b454db..f59efdc8e 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -7,3 +7,4 @@ pub mod layers; pub mod update_notice; pub mod open_progress; pub mod attribute_editor; +pub mod alias_editor;