Merge pull request #1460 from lalomalvi:codex/missing-font-prompt-hardening-pr

This commit is contained in:
Hakan Seven committed 2026-09-24 15:01:55 +03:00
commit 9cf9184d9a
7 files changed
+114 -15

No files matched your search

+2
View File
@@ -426,8 +426,10 @@ impl OpenCADStudio {
pub(super) fn control_state(&self) -> Value {
let tab = &self.tabs[self.active_tab];
let command = tab.active_cmd.as_deref().map(active_command_metadata);
let missing_fonts = crate::io::font_repo::missing_shx_fonts(&tab.scene.document);
json!({"ok":true,"protocol":1,"session_id":session_id(),"version":env!("OCS_APP_VERSION"),
"mode":if self.main_window.is_some(){"gui"}else{"headless"},"enabled":self.control.enabled,
"read_only":self.read_only,"missing_fonts":missing_fonts,
"document_id":tab.id,"revision":tab.edit_revision,"geometry_revision":tab.scene.geometry_epoch,"camera_revision":tab.scene.camera_generation,
"hand_seed":format!("{:X}",tab.scene.document.header.handle_seed.max(tab.scene.document.next_handle())),
"plugins":plugin_ids(),
+12
View File
@@ -1258,6 +1258,15 @@ pub(super) struct OpenCADStudio {
/// Missing SHX fonts of the last opened drawing, offered for download
/// from the community repository (see `crate::io::font_repo`).
missing_fonts: Option<Vec<String>>,
/// Drawing that produced `missing_fonts`; the active tab may change while
/// a download is in flight.
missing_fonts_path: Option<PathBuf>,
/// Prevent duplicate download tasks and keep modal contents visible while
/// the background request is running.
missing_fonts_downloading: bool,
/// Fonts skipped or unavailable during this process lifetime. Reopening a
/// drawing must not repeatedly nag for the same unresolved file.
suppressed_missing_fonts: rustc_hash::FxHashSet<String>,
/// Drawings handed to us by other launches while `opening` was busy.
/// `opening` is a single slot that a second `OpenPathPicked` would
/// overwrite, and `on_file_opened` drops any result arriving once it is
@@ -4178,6 +4187,9 @@ impl OpenCADStudio {
open_job_serial: 0,
recovery_report: None,
missing_fonts: None,
missing_fonts_path: None,
missing_fonts_downloading: false,
suppressed_missing_fonts: rustc_hash::FxHashSet::default(),
pending_opens: std::collections::VecDeque::new(),
active_interaction_index: None,
queued_interaction_indices: std::collections::VecDeque::new(),
+12 -2
View File
@@ -1911,12 +1911,22 @@ impl OpenCADStudio {
// fetch them from the community repository before the user
// studies garbled substitute text (unless recovery already
// owns the modal slot).
let missing = crate::io::font_repo::missing_shx_fonts(
let mut missing = crate::io::font_repo::missing_shx_fonts(
&self.tabs[i].scene.document,
);
if !missing.is_empty() && self.check_missing_fonts {
missing.retain(|name| {
!self
.suppressed_missing_fonts
.contains(&crate::io::font_repo::font_key(name))
});
// Read-only/MCP evaluation sessions must remain non-blocking:
// report missing fonts through control state, but never offer
// a network/download mutation from a read-only launch.
if !missing.is_empty() && self.check_missing_fonts && !self.read_only {
self.font_source_input = self.font_source_url.clone();
self.missing_fonts = Some(missing);
self.missing_fonts_path = self.tabs[i].current_path.clone();
self.missing_fonts_downloading = false;
self.active_modal = Some(crate::app::ModalKind::MissingFonts);
}
}
+57 -7
View File
@@ -1180,13 +1180,20 @@ impl OpenCADStudio {
Task::none()
}
Message::MissingFontsDownload => {
if self.missing_fonts_downloading {
return Task::none();
}
// Remember the source across sessions — and across drawings.
let source = self.font_source_input.trim().to_string();
if self.font_source_url != source {
self.font_source_url = source.clone();
self.save_config();
}
let fonts = self.missing_fonts.take().unwrap_or_default();
let fonts = self.missing_fonts.clone().unwrap_or_default();
if fonts.is_empty() {
return Task::none();
}
self.missing_fonts_downloading = true;
Task::perform(
async move {
let source = crate::io::font_repo::FontSource::from_url(&source);
@@ -1196,31 +1203,66 @@ impl OpenCADStudio {
)
}
Message::MissingFontsDismiss => {
self.missing_fonts = None;
if self.missing_fonts_downloading {
return Task::none();
}
for name in self.missing_fonts.take().unwrap_or_default() {
self.suppressed_missing_fonts
.insert(crate::io::font_repo::font_key(&name));
}
self.missing_fonts_path = None;
self.close_active_modal();
Task::none()
}
Message::MissingFontsResult(result) => {
self.missing_fonts = None;
self.close_active_modal();
self.missing_fonts_downloading = false;
match result {
Ok(pairs) if pairs.is_empty() => {
let unavailable = self.missing_fonts.take().unwrap_or_default();
for name in &unavailable {
self.suppressed_missing_fonts
.insert(crate::io::font_repo::font_key(name));
}
self.missing_fonts_path = None;
self.close_active_modal();
self.command_line.push_error(crate::t!(
"None of the missing fonts are in the community repository yet. Contribute them at github.com/huaninstratech/OpenCADStudio/tree/main/fonts."
"None of the requested fonts are available from the selected source. Substitute fonts remain active; this notice will not repeat during this session."
).as_ref());
}
Ok(pairs) => {
let requested = self.missing_fonts.take().unwrap_or_default();
let downloaded: rustc_hash::FxHashSet<String> = pairs
.iter()
.map(|(name, _)| crate::io::font_repo::font_key(name))
.collect();
let unavailable: Vec<String> = requested
.into_iter()
.filter(|name| {
!downloaded.contains(&crate::io::font_repo::font_key(name))
})
.collect();
for name in &unavailable {
self.suppressed_missing_fonts
.insert(crate::io::font_repo::font_key(name));
}
for (name, path) in &pairs {
self.command_line.push_output(crate::tf!(
"FONT Downloaded {name} → {path}",
path = path.display()
).as_ref());
}
if !unavailable.is_empty() {
self.command_line.push_info(crate::tf!(
"FONT Not available from the selected source: {fonts}. Substitute fonts remain active.",
fonts = unavailable.join(", ")
).as_ref());
}
// The downloaded files change glyph resolution for the
// whole drawing — reload it through the standard open
// pipeline so every wire is rebuilt with the real fonts.
let i = self.active_tab;
if let Some(path) = self.tabs[i].current_path.clone() {
let path = self.missing_fonts_path.take();
self.close_active_modal();
if let Some(path) = path {
return Task::done(Message::OpenExternal(path));
}
self.command_line.push_info(crate::t!(
@@ -1228,6 +1270,8 @@ impl OpenCADStudio {
).as_ref());
}
Err(e) => {
// Keep the prompt open: the user can correct a custom
// source or retry a transient network failure.
self.command_line.push_error(crate::tf!("Font download failed: {e}").as_ref());
}
}
@@ -8043,6 +8087,12 @@ impl OpenCADStudio {
if self.active_modal == Some(super::ModalKind::RecoveryPrompt) {
return self.update(Message::RecoveryDecline);
}
// The title-bar X, Escape, and MCP `close_modal` action mean
// the same thing as Skip for this prompt. Record that choice
// so reopening the drawing does not immediately nag again.
if self.active_modal == Some(super::ModalKind::MissingFonts) {
return self.update(Message::MissingFontsDismiss);
}
// The Options window's × and Esc behave like its Close button.
if self.active_modal == Some(super::ModalKind::Options) {
return self.update(Message::OptionsClose);
+1
View File
@@ -1675,6 +1675,7 @@ impl OpenCADStudio {
crate::ui::window::missing_fonts::view_window(
fonts,
&font_source,
self.missing_fonts_downloading,
flow,
)
})
+15
View File
@@ -42,6 +42,15 @@ pub enum FontSource {
Custom(String),
}
/// Stable key for comparing bare font names without path or case differences.
pub fn font_key(name: &str) -> String {
name.rsplit(['/', '\\'])
.next()
.unwrap_or(name)
.trim()
.to_ascii_lowercase()
}
impl FontSource {
/// Empty / whitespace input selects the community repository.
pub fn from_url(url: &str) -> Self {
@@ -265,6 +274,12 @@ mod tests {
assert_eq!(percent_encode_path("vn 30f.shx"), "vn%2030f.shx");
}
#[test]
fn font_keys_are_bare_trimmed_and_case_insensitive() {
assert_eq!(font_key(r#" C:\Fonts\ROMANS.SHX "#), "romans.shx");
assert_eq!(font_key("folder/simplex.shx"), "simplex.shx");
}
#[test]
fn rejects_a_non_listing_response() {
assert!(parse_contents("{\"message\":\"Not Found\"}").is_err());
+15 -6
View File
@@ -17,6 +17,7 @@ fn muted_style(theme: &Theme) -> text::Style {
pub fn view_window<'a>(
fonts: &'a [String],
font_source: &'a str,
downloading: bool,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let content_width = if matches!(sizing.width, Length::Fill) {
@@ -33,16 +34,24 @@ pub fn view_window<'a>(
.iter()
.map(|name| text(format!(" {name}")).size(12).into())
.collect();
let download = button(
text(if downloading {
crate::t!("Downloading...")
} else {
crate::t!("Download available")
})
.size(12),
)
.on_press_maybe((!downloading).then_some(Message::MissingFontsDownload))
.style(button::primary)
.padding([6, 14]);
let actions = row![
Space::new().width(content_width),
button(text(crate::t!("Skip")).size(12))
.on_press(Message::MissingFontsDismiss)
.on_press_maybe((!downloading).then_some(Message::MissingFontsDismiss))
.style(button::secondary)
.padding([6, 14]),
button(text(crate::t!("Download")).size(12))
.on_press(Message::MissingFontsDownload)
.style(button::primary)
.padding([6, 14]),
download,
]
.spacing(8);
@@ -50,7 +59,7 @@ pub fn view_window<'a>(
column![
text(crate::t!("Missing fonts")).size(20),
text(crate::t!(
"This drawing uses fonts that are not installed on this machine. Download them from the OpenCADStudio community font repository?"
"This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute."
))
.size(11)
.style(muted_style),