From 7811add6663bd6f69fb09b7eaa45f987f482ffdf Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 21 Jul 2026 19:43:11 +0300 Subject: [PATCH] feat(ui): persistent literal-space toggle on the command line '>' button after the Command: label turns the leading-'>' literal-space behavior into a mode: while on, Space stays in the line instead of submitting, until toggled off. State persists in settings.json; a hand-typed leading '>' still works for a single line. Co-Authored-By: Claude Fable 5 --- src/app/mod.rs | 4 +++ src/app/settings.rs | 5 ++++ src/app/update/file.rs | 2 ++ src/app/update/mod.rs | 17 +++++++---- src/ui/command_line.rs | 66 ++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index e22bfd4ff..066ea4d8b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1364,6 +1364,10 @@ pub enum Message { CommandHistoryNext, /// Toggle the dropdown listing the full command-line history. CommandHistoryToggle, + /// Toggle the persistent literal-space mode (the `>` button): while on, + /// every command line behaves as if it started with `>` — Space stays in + /// the input instead of submitting. Saved in the user config. + CommandLiteralToggle, /// Copy the full command-line history (every line) to the system /// clipboard as plain text — issue #232, so output can be pasted for /// debugging instead of screenshotted. diff --git a/src/app/settings.rs b/src/app/settings.rs index 2bba40dd6..f0b50cb55 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -112,6 +112,10 @@ pub struct UserSettings { /// Linked plugin source repositories (`owner/repo`) the marketplace installs /// from. pub plugin_repos: Vec, + /// Command-line literal-space mode: when on, Space stays in the input + /// instead of submitting (as if every line started with `>`), until the + /// user toggles it back off. + pub literal_spaces: bool, /// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single). pub texteditmode: bool, /// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false). @@ -143,6 +147,7 @@ impl Default for UserSettings { default_assoc_prompted: false, disabled_plugins: Vec::new(), plugin_repos: Vec::new(), + literal_spaces: false, texteditmode: false, textfill: true, backup_on_save: true, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 6e4c9d868..d2bc61863 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -75,6 +75,7 @@ impl OpenCADStudio { v }, plugin_repos: self.plugin_repos.clone(), + literal_spaces: self.command_line.literal_spaces, texteditmode: self.texteditmode, textfill: crate::scene::text::sdf_atlas::textfill(), backup_on_save: self.backup_on_save, @@ -96,6 +97,7 @@ impl OpenCADStudio { self.default_assoc_prompted = s.default_assoc_prompted; self.disabled_plugins = s.disabled_plugins.iter().cloned().collect(); self.plugin_repos = s.plugin_repos.clone(); + self.command_line.literal_spaces = s.literal_spaces; self.texteditmode = s.texteditmode; crate::scene::text::sdf_atlas::set_textfill(s.textfill); self.backup_on_save = s.backup_on_save; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index f16ef3a30..063bbf22d 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -770,7 +770,7 @@ impl OpenCADStudio { // the submit path, which tokenises multi-token lines — so a typed // token, a pasted `LINE 0,0 10,10`, or API-fed text all run their // spaces as step separators. A leading `>` keeps spaces literal. - if !s.starts_with('>') && s.contains(' ') { + if !self.command_line.literal_spaces && !s.starts_with('>') && s.contains(' ') { self.command_line.input = s; return self.update(Message::CommandSubmit); } @@ -856,6 +856,12 @@ impl OpenCADStudio { Task::none() } + Message::CommandLiteralToggle => { + self.command_line.literal_spaces = !self.command_line.literal_spaces; + self.save_config(); + self.focus_cmd_input() + } + Message::CommandHistoryToggle => { self.command_line.toggle_history(); // On open, snapshot the current log into the read-only editor @@ -924,10 +930,11 @@ impl OpenCADStudio { self.mtext_type(" "); return Task::none(); } - // A leading `>` puts the command line in "literal space" mode so - // the user can type arguments that contain spaces; otherwise - // Space works like Enter. The `>` is stripped on submit. - if self.command_line.input.starts_with('>') { + // A leading `>` (or the persistent `>` toggle) puts the command + // line in "literal space" mode so the user can type arguments + // that contain spaces; otherwise Space works like Enter. The + // typed `>` is stripped on submit. + if self.command_line.literal_spaces || self.command_line.input.starts_with('>') { self.command_line.input.push(' '); return Task::none(); } diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index 19f7770c1..fa55aea16 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -5,7 +5,7 @@ use iced::time::Instant; use crate::app::Message; use crate::command::CmdOption; use iced::widget::{ - button, column, container, opaque, row, text, text_editor, text_input, Space, + button, column, container, opaque, row, text, text_editor, text_input, tooltip, Space, }; use iced::{Background, Border, Color, Element, Length, Theme}; @@ -27,6 +27,10 @@ const MAX_HISTORY: usize = 64; #[derive(Clone, Default)] pub struct CommandLine { pub input: String, + /// Persistent literal-space mode (the `>` toggle button): while on, every + /// line behaves as if it started with `>` — Space stays in the input + /// instead of submitting. Saved in the user config. + pub literal_spaces: bool, pub history: Vec, /// Commands the user has typed (for ↑/↓ recall). Holds the raw typed /// strings so the line can be re-edited; distinct from `recent_commands`. @@ -383,6 +387,64 @@ impl CommandLine { } }); let prompt = container(text("Command:").size(11).color(PROMPT_COLOR)).padding([5, 8]); + // Literal-space toggle: while active, every line behaves as if it + // started with `>` — Space stays in the line instead of submitting, so + // arguments with spaces (text strings, paths, `UCS Z 90` as one line) + // can be typed. Persists until toggled off; a hand-typed leading `>` + // lights the button too (same mode, one line only). + let literal_active = self.literal_spaces || self.input.starts_with('>'); + let literal_btn = button(text(">").size(11).color(if literal_active { + Color::WHITE + } else { + PROMPT_COLOR + })) + .on_press(Message::CommandLiteralToggle) + .padding([2, 6]) + .style(move |_: &Theme, status| { + let bg = if literal_active { + Color { + r: 0.28, + g: 0.40, + b: 0.56, + a: 1.0, + } + } else if matches!(status, button::Status::Hovered) { + Color { + r: 0.22, + g: 0.30, + b: 0.42, + a: 1.0, + } + } else { + INPUT_ROW_BG + }; + button::Style { + background: Some(Background::Color(bg)), + text_color: Color::WHITE, + border: Border { + color: BORDER_COLOR, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + } + }); + let literal_tip = container( + text("Literal spaces: Space stays in the line instead of running the command (same as typing a leading '>'). Stays on until toggled off.") + .size(11) + .color(Color::WHITE), + ) + .padding([3, 6]) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(PANEL_BG)), + border: Border { + color: BORDER_COLOR, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + }); + let literal_btn = tooltip(literal_btn, literal_tip, tooltip::Position::Top).gap(4); // While dynamic input is capturing keystrokes, the command-line // text field is left without an `on_input` handler so it can't // grab focus or swallow numeric keys — those flow through the @@ -509,7 +571,7 @@ impl CommandLine { ..Default::default() }) .padding([2, 6]); - let input_row = row![prompt, input, dropdown_btn] + let input_row = row![prompt, literal_btn, input, dropdown_btn] .spacing(4) .align_y(iced::Center);