From d329dd05e1c7b542bbe2b4ca834ecec931c96ce9 Mon Sep 17 00:00:00 2001 From: SBaksa Date: Sun, 8 Nov 2026 14:32:34 +1100 Subject: [PATCH] Export for Screens: a leading dot in an artboard name is a dash A name like .draft would otherwise write a hidden file on Unix-like systems. --- crates/engine/src/cmd/fileio/export_names.rs | 11 ++++++++--- crates/engine/src/cmd/fileio/tests_screens.rs | 9 +++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/cmd/fileio/export_names.rs b/crates/engine/src/cmd/fileio/export_names.rs index 5ff0add25..5f2c14bfd 100644 --- a/crates/engine/src/cmd/fileio/export_names.rs +++ b/crates/engine/src/cmd/fileio/export_names.rs @@ -31,12 +31,17 @@ fn unique_file_names<'a>(names: impl IntoIterator) -> /// [`unique_file_names`] keeping `.` in a name (an artboard called `example.artboard` exports as /// `example.artboard.png`, #6143). A name of only dots (`.` and `..` name other folders) takes the -/// fallback; the caller still runs each stem through `safe_file_name`, which drops trailing periods -/// and guards Windows device names. +/// fallback, and leading dots are dashes (`.draft` would be a hidden file); the caller still runs +/// each stem through `safe_file_name`, which drops trailing periods and guards Windows device names. pub(crate) fn unique_file_names_keeping_dots<'a>(names: impl IntoIterator) -> Vec { distinct_names(names.into_iter().map(|(name, fallback)| { let base: String = name.chars().map(|c| if c.is_alphanumeric() || matches!(c, '-' | '_' | '.') { c } else { '-' }).collect(); - if base.trim_matches('.').is_empty() { fallback } else { base } + if base.trim_matches('.').is_empty() { + return fallback; + } + // A leading dot would hide the file or folder on Unix-like systems: it is a dash, as before. + let rest = base.trim_start_matches('.'); + format!("{}{rest}", "-".repeat(base.len() - rest.len())) })) } diff --git a/crates/engine/src/cmd/fileio/tests_screens.rs b/crates/engine/src/cmd/fileio/tests_screens.rs index ff2358aa0..b0dcd368e 100644 --- a/crates/engine/src/cmd/fileio/tests_screens.rs +++ b/crates/engine/src/cmd/fileio/tests_screens.rs @@ -149,15 +149,16 @@ fn device_names_and_trailing_periods_follow_windows_naming_rules() { } /// A dot in an artboard's name stays a dot (`example.artboard.png`, #6143); a name of only dots, or -/// ending in one, still can't name another folder or end in a period, and `/` and `:` are dashes. +/// ending in one, still can't name another folder or end in a period, a leading dot is a dash (no +/// hidden `.draft.png`), and `/` and `:` are dashes. #[test] fn an_artboard_name_keeps_its_dots() { - let mut s = session(5); - for (index, name) in ["example.artboard", "v1.2.final", "..", "a/b:c", "trail."].iter().enumerate() { + let mut s = session(7); + for (index, name) in ["example.artboard", "v1.2.final", "..", "a/b:c", "trail.", ".draft", "..x.y"].iter().enumerate() { s.execute("artboard.setProps", &json!({"index": index, "name": name})).unwrap(); } let r = export(&mut s, json!({"formats": [{"format": "png"}]})); - assert_eq!(names(&r), ["example.artboard.png", "v1.2.final.png", "Artboard-3.png", "a-b-c.png", "trail.png"]); + assert_eq!(names(&r), ["example.artboard.png", "v1.2.final.png", "Artboard-3.png", "a-b-c.png", "trail.png", "-draft.png", "--x.y.png"]); } /// `first_file` names the file the export writes first, for the same params: the item's file name,