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 <noreply@anthropic.com>
This commit is contained in:
parent
c0499a1d3c
commit
7811add666
5 changed files with 87 additions and 7 deletions
|
|
@ -1364,6 +1364,10 @@ pub enum Message {
|
||||||
CommandHistoryNext,
|
CommandHistoryNext,
|
||||||
/// Toggle the dropdown listing the full command-line history.
|
/// Toggle the dropdown listing the full command-line history.
|
||||||
CommandHistoryToggle,
|
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
|
/// Copy the full command-line history (every line) to the system
|
||||||
/// clipboard as plain text — issue #232, so output can be pasted for
|
/// clipboard as plain text — issue #232, so output can be pasted for
|
||||||
/// debugging instead of screenshotted.
|
/// debugging instead of screenshotted.
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,10 @@ pub struct UserSettings {
|
||||||
/// Linked plugin source repositories (`owner/repo`) the marketplace installs
|
/// Linked plugin source repositories (`owner/repo`) the marketplace installs
|
||||||
/// from.
|
/// from.
|
||||||
pub plugin_repos: Vec<String>,
|
pub plugin_repos: Vec<String>,
|
||||||
|
/// 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).
|
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
||||||
pub texteditmode: bool,
|
pub texteditmode: bool,
|
||||||
/// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false).
|
/// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false).
|
||||||
|
|
@ -143,6 +147,7 @@ impl Default for UserSettings {
|
||||||
default_assoc_prompted: false,
|
default_assoc_prompted: false,
|
||||||
disabled_plugins: Vec::new(),
|
disabled_plugins: Vec::new(),
|
||||||
plugin_repos: Vec::new(),
|
plugin_repos: Vec::new(),
|
||||||
|
literal_spaces: false,
|
||||||
texteditmode: false,
|
texteditmode: false,
|
||||||
textfill: true,
|
textfill: true,
|
||||||
backup_on_save: true,
|
backup_on_save: true,
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ impl OpenCADStudio {
|
||||||
v
|
v
|
||||||
},
|
},
|
||||||
plugin_repos: self.plugin_repos.clone(),
|
plugin_repos: self.plugin_repos.clone(),
|
||||||
|
literal_spaces: self.command_line.literal_spaces,
|
||||||
texteditmode: self.texteditmode,
|
texteditmode: self.texteditmode,
|
||||||
textfill: crate::scene::text::sdf_atlas::textfill(),
|
textfill: crate::scene::text::sdf_atlas::textfill(),
|
||||||
backup_on_save: self.backup_on_save,
|
backup_on_save: self.backup_on_save,
|
||||||
|
|
@ -96,6 +97,7 @@ impl OpenCADStudio {
|
||||||
self.default_assoc_prompted = s.default_assoc_prompted;
|
self.default_assoc_prompted = s.default_assoc_prompted;
|
||||||
self.disabled_plugins = s.disabled_plugins.iter().cloned().collect();
|
self.disabled_plugins = s.disabled_plugins.iter().cloned().collect();
|
||||||
self.plugin_repos = s.plugin_repos.clone();
|
self.plugin_repos = s.plugin_repos.clone();
|
||||||
|
self.command_line.literal_spaces = s.literal_spaces;
|
||||||
self.texteditmode = s.texteditmode;
|
self.texteditmode = s.texteditmode;
|
||||||
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
|
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
|
||||||
self.backup_on_save = s.backup_on_save;
|
self.backup_on_save = s.backup_on_save;
|
||||||
|
|
|
||||||
|
|
@ -770,7 +770,7 @@ impl OpenCADStudio {
|
||||||
// the submit path, which tokenises multi-token lines — so a typed
|
// 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
|
// token, a pasted `LINE 0,0 10,10`, or API-fed text all run their
|
||||||
// spaces as step separators. A leading `>` keeps spaces literal.
|
// 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;
|
self.command_line.input = s;
|
||||||
return self.update(Message::CommandSubmit);
|
return self.update(Message::CommandSubmit);
|
||||||
}
|
}
|
||||||
|
|
@ -856,6 +856,12 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::CommandLiteralToggle => {
|
||||||
|
self.command_line.literal_spaces = !self.command_line.literal_spaces;
|
||||||
|
self.save_config();
|
||||||
|
self.focus_cmd_input()
|
||||||
|
}
|
||||||
|
|
||||||
Message::CommandHistoryToggle => {
|
Message::CommandHistoryToggle => {
|
||||||
self.command_line.toggle_history();
|
self.command_line.toggle_history();
|
||||||
// On open, snapshot the current log into the read-only editor
|
// On open, snapshot the current log into the read-only editor
|
||||||
|
|
@ -924,10 +930,11 @@ impl OpenCADStudio {
|
||||||
self.mtext_type(" ");
|
self.mtext_type(" ");
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
// A leading `>` puts the command line in "literal space" mode so
|
// A leading `>` (or the persistent `>` toggle) puts the command
|
||||||
// the user can type arguments that contain spaces; otherwise
|
// line in "literal space" mode so the user can type arguments
|
||||||
// Space works like Enter. The `>` is stripped on submit.
|
// that contain spaces; otherwise Space works like Enter. The
|
||||||
if self.command_line.input.starts_with('>') {
|
// typed `>` is stripped on submit.
|
||||||
|
if self.command_line.literal_spaces || self.command_line.input.starts_with('>') {
|
||||||
self.command_line.input.push(' ');
|
self.command_line.input.push(' ');
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use iced::time::Instant;
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::command::CmdOption;
|
use crate::command::CmdOption;
|
||||||
use iced::widget::{
|
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};
|
use iced::{Background, Border, Color, Element, Length, Theme};
|
||||||
|
|
||||||
|
|
@ -27,6 +27,10 @@ const MAX_HISTORY: usize = 64;
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct CommandLine {
|
pub struct CommandLine {
|
||||||
pub input: String,
|
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<HistoryEntry>,
|
pub history: Vec<HistoryEntry>,
|
||||||
/// Commands the user has typed (for ↑/↓ recall). Holds the raw typed
|
/// Commands the user has typed (for ↑/↓ recall). Holds the raw typed
|
||||||
/// strings so the line can be re-edited; distinct from `recent_commands`.
|
/// 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]);
|
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
|
// While dynamic input is capturing keystrokes, the command-line
|
||||||
// text field is left without an `on_input` handler so it can't
|
// text field is left without an `on_input` handler so it can't
|
||||||
// grab focus or swallow numeric keys — those flow through the
|
// grab focus or swallow numeric keys — those flow through the
|
||||||
|
|
@ -509,7 +571,7 @@ impl CommandLine {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.padding([2, 6]);
|
.padding([2, 6]);
|
||||||
let input_row = row![prompt, input, dropdown_btn]
|
let input_row = row![prompt, literal_btn, input, dropdown_btn]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.align_y(iced::Center);
|
.align_y(iced::Center);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue