Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2c6764472 |
No files matched your search
Generated
+2
@@ -4251,6 +4251,7 @@ name = "vectorcraft"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"block2 0.6.2",
|
||||
"clipboard-win",
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -4259,6 +4260,7 @@ dependencies = [
|
||||
"muda",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
"rfd",
|
||||
"serde_json",
|
||||
"vectorcraft-engine",
|
||||
|
||||
+2
-1
@@ -42,7 +42,7 @@ since 2026-10-01.
|
||||
| **Bundled content:** brush, symbol, style and swatch libraries | ~30% | Ours are original and generated in code, and far fewer than Illustrator ships; brush and symbol libraries are still missing |
|
||||
| **Performance** | unverified | Multithreaded, off-thread rendering and caches are in place. The last budget run (2026-10-01, loaded machine) measured 290 ms for a 50k-path fit against a 16 ms budget. Re-run `vectorcraft-cli perf` on an idle machine |
|
||||
| **Robustness** | good, new | ~2,840 tests, property tests, no panics in shipped code (lints, the `guard` safety net, import fuzzing), Data Recovery. Missing: a corpus of real-world files, Windows/Linux/browser QA |
|
||||
| **Platforms and 1.0 polish** | ~60% | The macOS app and the web build work; hybrid-graphics laptops render on the power-saving GPU by default (#306); web drops land in the document they were dropped on (#359); Windows/Linux packaging and accessibility are pending |
|
||||
| **Platforms and 1.0 polish** | ~60% | The macOS app and the web build work, and Finder opens every readable format with the macOS app (#295, #354); hybrid-graphics laptops render on the power-saving GPU by default (#306); web drops land in the document they were dropped on (#359); Windows/Linux packaging and accessibility are pending |
|
||||
| **Agent automation** | beyond Illustrator | Every command, gesture and dialog is drivable over MCP, the CLI and the control channel |
|
||||
|
||||
### Where we're lacking (in priority order)
|
||||
@@ -98,6 +98,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- The dock's double arrow collapses Properties, Layers and Libraries to icons at the top of the icon column, popping out like the other icon panels, and expands them again (`window.collapseDock`, kept with the preferences and in saved workspaces). The icon column's own arrow (expanding the icon panels into a full column) is not done.
|
||||
- Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland) and Japanese ship complete, and Czech and Brazilian Portuguese cover every menu label (and more), with system-locale detection on macOS, Windows and Linux, a VectorCraft ▸ Language menu and a Language preference. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins).
|
||||
- Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool.
|
||||
- macOS: Finder opens every format File › Open reads with VectorCraft (double-click, Open With, a drop on the Dock icon, `open -a`), at launch or while it runs; the release and `cargo xtask bundle` apps share one `Info.plist` that declares those document types, owning only VectorCraft's own formats.
|
||||
- A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, Remove Anchor Points when anchors are selected, masks, compound paths, guides, Transform, Arrange, Select, Export Selection), or the view commands on empty canvas.
|
||||
- Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close).
|
||||
- Four brightness themes, persistent preferences.
|
||||
|
||||
@@ -31,7 +31,10 @@ clipboard-win = { workspace = true }
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
muda = { version = "0.21", default-features = false }
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSTextInputContext"] }
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSTextInputContext"] }
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs); both already come with winit, muda and rfd.
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSString", "NSURL"] }
|
||||
block2 = "0.6"
|
||||
|
||||
[build-dependencies]
|
||||
# Icon + VERSIONINFO for vectorcraft.exe; build.rs only uses it when targeting Windows.
|
||||
|
||||
@@ -11,6 +11,8 @@ mod clipboard;
|
||||
mod control_server;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod native_menu;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod open_documents;
|
||||
mod printing;
|
||||
mod window;
|
||||
|
||||
@@ -30,6 +32,7 @@ impl eframe::App for App {
|
||||
if let Some(m) = &mut self.1 {
|
||||
m.poll(&mut self.0);
|
||||
}
|
||||
open_files(&mut self.0, open_documents::take());
|
||||
}
|
||||
self.0.logic(ctx);
|
||||
window::track(ctx, &mut self.0.ui.window);
|
||||
@@ -52,6 +55,17 @@ impl eframe::App for App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Open files handed to the app (command line, macOS Finder and Dock) as documents. A file that
|
||||
/// can't be opened is reported in the status bar and on stderr; the others still open.
|
||||
fn open_files(app: &mut VectorcraftApp, files: Vec<String>) {
|
||||
for f in files {
|
||||
if let Err(e) = vectorcraft_ui_egui::io::open_path(app, &f) {
|
||||
eprintln!("vectorcraft: {f}: {e}");
|
||||
app.status(format!("Couldn't open {}: {e}", fileio::file_name(&f)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tell the macOS input method to drop its composition (the Type tool kept the marked text as
|
||||
/// typed). winit's IME toggle only clears its own copy, so the IME would type it again.
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -302,6 +316,9 @@ fn main() -> eframe::Result {
|
||||
if let eframe::egui_wgpu::WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup {
|
||||
create.power_preference = power;
|
||||
}
|
||||
// Files opened from Finder and the Dock arrive as events, not arguments.
|
||||
#[cfg(target_os = "macos")]
|
||||
open_documents::install();
|
||||
eframe::run_native(
|
||||
"VectorCraft",
|
||||
options,
|
||||
@@ -334,11 +351,9 @@ fn main() -> eframe::Result {
|
||||
let rx = control_server::start(port, cc.egui_ctx.clone());
|
||||
app = app.with_control(rx);
|
||||
}
|
||||
for f in files {
|
||||
if let Err(e) = vectorcraft_ui_egui::io::open_path(&mut app, &f) {
|
||||
eprintln!("vectorcraft: {f}: {e}");
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
open_documents::set_ui(&cc.egui_ctx);
|
||||
open_files(&mut app, files);
|
||||
Ok(Box::new(App(
|
||||
app,
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -367,6 +382,35 @@ mod tests {
|
||||
assert_eq!(power_preference(None, Some(PowerPreference::None)), PowerPreference::None);
|
||||
}
|
||||
|
||||
/// The file extensions the macOS bundle declares: its document types and its own exported type.
|
||||
fn plist_extensions(plist: &str) -> Vec<&str> {
|
||||
["<key>CFBundleTypeExtensions</key>", "<key>public.filename-extension</key>"]
|
||||
.iter()
|
||||
.flat_map(|key| plist.split(key).skip(1))
|
||||
.filter_map(|rest| rest.split("</array>").next())
|
||||
.flat_map(|array| array.split("<string>").skip(1))
|
||||
.filter_map(|s| s.split("</string>").next())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Finder offers the app for every file File › Open reads (#295, #354), takes over no other
|
||||
/// app's files, and hands them to the app rather than to AppKit's document machinery.
|
||||
#[test]
|
||||
fn the_macos_bundle_opens_every_readable_format() {
|
||||
let plist = include_str!("../../../packaging/macos/Info.plist.in");
|
||||
let declared = plist_extensions(plist);
|
||||
for e in fileio::OPEN_EXTS {
|
||||
assert!(declared.contains(e), "Info.plist.in doesn't declare .{e}");
|
||||
}
|
||||
for e in &declared {
|
||||
assert!(fileio::OPEN_EXTS.contains(e), "Info.plist.in declares .{e}, which the app doesn't open");
|
||||
}
|
||||
assert!(!plist.contains("<key>NSDocumentClass</key>"), "not an NSDocument app: AppKit would refuse the files");
|
||||
let types = plist.matches("<key>CFBundleTypeName</key>").count();
|
||||
assert!(types > 1 && plist.matches("<key>LSHandlerRank</key>").count() == types, "every document type has a rank");
|
||||
assert_eq!(plist.matches("<string>Owner</string>").count(), 2, "only VectorCraft documents and templates are owned");
|
||||
}
|
||||
|
||||
/// The preference the engine saves is the one the app reads back before the window opens.
|
||||
#[test]
|
||||
fn the_saved_engine_preference_is_found() {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
//! macOS: files opened from Finder (double-click, Open With, a drop on the Dock icon) or with
|
||||
//! `open -a VectorCraft file`.
|
||||
//!
|
||||
//! macOS doesn't pass these on the command line: AppKit hands them to the application delegate's
|
||||
//! `application:openURLs:`, and when the delegate has no such method it asks `NSDocumentController`,
|
||||
//! which fails with "VectorCraft cannot open files in the … format" because VectorCraft isn't an
|
||||
//! `NSDocument` app. winit 0.30's delegate doesn't implement the method, so [`install`] adds it to
|
||||
//! the delegate's class when the app is about to finish launching: after winit set the delegate and
|
||||
//! before AppKit delivers the files the app was launched with. The handler queues the paths and
|
||||
//! wakes the UI, which opens them with [`take`] like files named on the command line.
|
||||
//!
|
||||
//! Adding a method to another crate's Objective-C class has no safe API, hence the scoped
|
||||
//! `unsafe_code` allowance (winit overrides `sendEvent:` on `NSApplication` the same way).
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::{Mutex, OnceLock, PoisonError};
|
||||
|
||||
use block2::RcBlock;
|
||||
use objc2::MainThreadMarker;
|
||||
use objc2::runtime::{AnyObject, Imp, Sel};
|
||||
use objc2::sel;
|
||||
use objc2_app_kit::{NSApplication, NSApplicationWillFinishLaunchingNotification};
|
||||
use objc2_foundation::{NSArray, NSNotification, NSNotificationCenter, NSURL};
|
||||
|
||||
/// Paths that arrived and haven't been opened yet.
|
||||
static PENDING: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
/// The UI to wake when files arrive (unset until the window exists; the first frame opens them).
|
||||
static UI: OnceLock<egui::Context> = OnceLock::new();
|
||||
|
||||
/// Handle open-document events from now on. Call before the event loop runs.
|
||||
pub fn install() {
|
||||
let block = RcBlock::new(|_: NonNull<NSNotification>| add_open_urls_method());
|
||||
// SAFETY: the name is AppKit's notification constant; no object or queue filter, so the block
|
||||
// runs on the posting thread (AppKit posts it on the main thread). The notification center
|
||||
// keeps the registration (and the block) for the life of the app.
|
||||
unsafe {
|
||||
NSNotificationCenter::defaultCenter().addObserverForName_object_queue_usingBlock(
|
||||
Some(NSApplicationWillFinishLaunchingNotification),
|
||||
None,
|
||||
None,
|
||||
&block,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Wake `ctx` when files arrive while the app runs.
|
||||
pub fn set_ui(ctx: &egui::Context) {
|
||||
// Set once; a second call would hand over the same context.
|
||||
let _ = UI.set(ctx.clone());
|
||||
}
|
||||
|
||||
/// The paths that arrived since the last call.
|
||||
pub fn take() -> Vec<String> {
|
||||
std::mem::take(&mut *PENDING.lock().unwrap_or_else(PoisonError::into_inner))
|
||||
}
|
||||
|
||||
/// `application:openURLs:` on the application delegate's class.
|
||||
fn add_open_urls_method() {
|
||||
let Some(mtm) = MainThreadMarker::new() else { return };
|
||||
let Some(delegate) = NSApplication::sharedApplication(mtm).delegate() else {
|
||||
log::warn!("vectorcraft: no application delegate; files opened from Finder won't open");
|
||||
return;
|
||||
};
|
||||
let class = AsRef::<AnyObject>::as_ref(&*delegate).class();
|
||||
let handler: extern "C-unwind" fn(&AnyObject, Sel, &AnyObject, &NSArray<NSURL>) = open_urls;
|
||||
// SAFETY: `Imp` is the untyped form of a method implementation; the Objective-C runtime calls
|
||||
// it with the arguments the type encoding declares, which match `open_urls`: no return value
|
||||
// (`v`), the receiver (`@`), the selector (`:`), the application (`@`) and the URL array (`@`).
|
||||
let added = unsafe {
|
||||
let imp = std::mem::transmute::<extern "C-unwind" fn(&AnyObject, Sel, &AnyObject, &NSArray<NSURL>), Imp>(handler);
|
||||
objc2::ffi::class_addMethod(std::ptr::from_ref(class).cast_mut(), sel!(application:openURLs:), imp, c"v@:@@".as_ptr())
|
||||
};
|
||||
if !added.as_bool() {
|
||||
log::warn!("vectorcraft: the application delegate already handles open-document events");
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue the files among `urls` and wake the UI. Never panics: it runs inside AppKit.
|
||||
extern "C-unwind" fn open_urls(_delegate: &AnyObject, _cmd: Sel, _app: &AnyObject, urls: &NSArray<NSURL>) {
|
||||
let paths = urls.iter().filter_map(|u| u.to_file_path()).map(|p| p.to_string_lossy().into_owned());
|
||||
PENDING.lock().unwrap_or_else(PoisonError::into_inner).extend(paths);
|
||||
if let Some(ctx) = UI.get() {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use objc2::runtime::NSObject;
|
||||
use objc2_foundation::NSString;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Files queue until the UI takes them; URLs that aren't files are left out.
|
||||
#[test]
|
||||
fn opened_files_wait_for_the_ui() {
|
||||
let web = NSURL::URLWithString(&NSString::from_str("https://example.com/a.svg")).unwrap();
|
||||
let urls = NSArray::from_retained_slice(&[NSURL::from_file_path("/tmp/a b.pdf").unwrap(), web, NSURL::from_file_path("/tmp/c.ai").unwrap()]);
|
||||
let any = NSObject::new();
|
||||
open_urls(&any, sel!(application:openURLs:), &any, &urls);
|
||||
assert_eq!(take(), ["/tmp/a b.pdf", "/tmp/c.ai"]);
|
||||
assert!(take().is_empty());
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -67,7 +67,13 @@ The rules are in craftrules `standards/fonts.md`, the build option in
|
||||
|
||||
- `Info.plist` is generated from `Info.plist.in` (bundle id `ai.storyteller.vectorcraft`,
|
||||
`LSMinimumSystemVersion` 11.0, the version and the build commit). The icon is
|
||||
`assets/app-icon/vectorcraft.icns`.
|
||||
`assets/app-icon/vectorcraft.icns`. `cargo xtask bundle` fills in the same template for its
|
||||
development app.
|
||||
- **Document types:** `Info.plist.in` declares every format File › Open reads, so Finder opens
|
||||
them with VectorCraft (double-click, Open With, a drop on the Dock icon). VectorCraft owns its own
|
||||
formats and is an alternate for the others. A test keeps the list in step with the engine's
|
||||
`OPEN_EXTS`. AppKit hands the files to the app as open-document events, which
|
||||
`apps/vectorcraft/src/open_documents.rs` opens like files named on the command line.
|
||||
- **Signing** goes inside-out with the hardened runtime and a secure timestamp: the executable
|
||||
first, then the bundle, with no `--deep` on the final signature. The entitlements
|
||||
(`entitlements.plist`) are deliberately empty.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<!-- Template: packaging/macos/package.sh substitutes @VERSION@ (full, e.g. 0.2.0-rc.1),
|
||||
@SHORT_VERSION@ (numeric X.Y.Z) and @BUILD_SHA@. -->
|
||||
<!-- Template: packaging/macos/package.sh (release) and `cargo xtask bundle` (development)
|
||||
substitute @VERSION@ (full, e.g. 0.2.0-rc.1), @SHORT_VERSION@ (numeric X.Y.Z) and @BUILD_SHA@. -->
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
@@ -45,6 +45,73 @@
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © the VectorCraft authors. MIT OR Apache-2.0.</string>
|
||||
|
||||
<!-- Every format File › Open reads (OPEN_EXTS in crates/engine/src/cmd/fileio/mod.rs; a test in
|
||||
apps/vectorcraft checks this list), so Finder offers VectorCraft for them (Open With,
|
||||
double-click, Dock drop). VectorCraft owns its own formats; for the others it is an
|
||||
alternate, so it doesn't take over another app's files. The app isn't NSDocument-based (no
|
||||
NSDocumentClass): it opens these files itself (apps/vectorcraft/src/open_documents.rs). -->
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key><string>VectorCraft Document</string>
|
||||
<key>LSItemContentTypes</key><array><string>ai.storyteller.vectorcraft.document</string></array>
|
||||
<key>CFBundleTypeRole</key><string>Editor</string>
|
||||
<key>LSHandlerRank</key><string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key><string>VectorCraft Template</string>
|
||||
<key>CFBundleTypeExtensions</key><array><string>vctemplate</string></array>
|
||||
<key>CFBundleTypeRole</key><string>Editor</string>
|
||||
<key>LSHandlerRank</key><string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key><string>SVG</string>
|
||||
<key>CFBundleTypeExtensions</key><array><string>svg</string><string>svgz</string></array>
|
||||
<key>CFBundleTypeRole</key><string>Editor</string>
|
||||
<key>LSHandlerRank</key><string>Alternate</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key><string>PDF</string>
|
||||
<key>CFBundleTypeExtensions</key><array><string>pdf</string></array>
|
||||
<key>CFBundleTypeRole</key><string>Editor</string>
|
||||
<key>LSHandlerRank</key><string>Alternate</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key><string>PDF-compatible .ai</string>
|
||||
<key>CFBundleTypeExtensions</key><array><string>ai</string><string>ait</string></array>
|
||||
<key>CFBundleTypeRole</key><string>Editor</string>
|
||||
<key>LSHandlerRank</key><string>Alternate</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key><string>EPS</string>
|
||||
<key>CFBundleTypeExtensions</key><array><string>eps</string></array>
|
||||
<key>CFBundleTypeRole</key><string>Editor</string>
|
||||
<key>LSHandlerRank</key><string>Alternate</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key><string>DXF</string>
|
||||
<key>CFBundleTypeExtensions</key><array><string>dxf</string></array>
|
||||
<key>CFBundleTypeRole</key><string>Editor</string>
|
||||
<key>LSHandlerRank</key><string>Alternate</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key><string>EMF and WMF</string>
|
||||
<key>CFBundleTypeExtensions</key><array><string>emf</string><string>wmf</string></array>
|
||||
<key>CFBundleTypeRole</key><string>Editor</string>
|
||||
<key>LSHandlerRank</key><string>Alternate</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key><string>Image</string>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>png</string><string>jpg</string><string>jpeg</string><string>gif</string>
|
||||
<string>webp</string><string>tif</string><string>tiff</string><string>bmp</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key><string>Editor</string>
|
||||
<key>LSHandlerRank</key><string>Alternate</string>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
<!-- VectorCraft's own format (.drawcraft is its name from before the rename). -->
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
|
||||
+16
-29
@@ -21,35 +21,20 @@ pub fn run(root: &Path) -> Result<(), String> {
|
||||
std::fs::copy(target.join("release/vectorcraft"), app.join("MacOS/VectorCraft")).map_err(|e| format!("copy app: {e}"))?;
|
||||
std::fs::copy(target.join("release/vectorcraft-cli"), app.join("MacOS/vectorcraft-cli")).map_err(|e| format!("copy cli: {e}"))?;
|
||||
std::fs::copy(root.join("assets/app-icon/vectorcraft.icns"), app.join("Resources/VectorCraft.icns")).map_err(|e| format!("copy icon: {e}"))?;
|
||||
std::fs::write(app.join("Info.plist"), info_plist(env!("CARGO_PKG_VERSION"))).map_err(|e| e.to_string())?;
|
||||
let sha = std::env::var("VECTORCRAFT_BUILD_SHA").unwrap_or_else(|_| "unknown".into());
|
||||
std::fs::write(app.join("Info.plist"), info_plist(env!("CARGO_PKG_VERSION"), &sha)).map_err(|e| e.to_string())?;
|
||||
println!("built {}", root.join("dist/VectorCraft.app").display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The development bundle's `Info.plist`.
|
||||
fn info_plist(version: &str) -> String {
|
||||
format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>CFBundleName</key><string>VectorCraft</string>
|
||||
<key>CFBundleDisplayName</key><string>VectorCraft</string>
|
||||
<key>CFBundleIdentifier</key><string>ai.storyteller.vectorcraft</string>
|
||||
<key>CFBundleExecutable</key><string>VectorCraft</string>
|
||||
<key>CFBundleIconFile</key><string>VectorCraft</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key><string>{version}</string>
|
||||
<key>CFBundleVersion</key><string>{version}</string>
|
||||
<key>NSHighResolutionCapable</key><true/>
|
||||
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
||||
<key>CFBundleDocumentTypes</key><array>
|
||||
<dict><key>CFBundleTypeName</key><string>VectorCraft Document</string><key>CFBundleTypeExtensions</key><array><string>vectorcraft</string></array><key>CFBundleTypeRole</key><string>Editor</string></dict>
|
||||
<dict><key>CFBundleTypeName</key><string>SVG</string><key>CFBundleTypeExtensions</key><array><string>svg</string></array><key>CFBundleTypeRole</key><string>Editor</string></dict>
|
||||
<dict><key>CFBundleTypeName</key><string>PDF Document</string><key>CFBundleTypeExtensions</key><array><string>pdf</string><string>ai</string></array><key>CFBundleTypeRole</key><string>Viewer</string></dict>
|
||||
</array>
|
||||
</dict></plist>
|
||||
"#
|
||||
)
|
||||
/// The release `Info.plist` template, which `packaging/macos/package.sh` fills in the same way: the
|
||||
/// development bundle declares the same document types (Finder opens them with the app).
|
||||
const INFO_PLIST: &str = include_str!("../../packaging/macos/Info.plist.in");
|
||||
|
||||
/// The bundle's `Info.plist`: [`INFO_PLIST`] for `version` and the build commit `sha`.
|
||||
fn info_plist(version: &str, sha: &str) -> String {
|
||||
let short = version.split('-').next().unwrap_or(version);
|
||||
INFO_PLIST.replace("@VERSION@", version).replace("@SHORT_VERSION@", short).replace("@BUILD_SHA@", sha)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -88,9 +73,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dev_bundle_plist_names_types_neutrally() {
|
||||
let p = info_plist("1.2.3");
|
||||
assert!(p.contains("<key>CFBundleShortVersionString</key><string>1.2.3</string>"));
|
||||
assert!(p.contains("<string>VectorCraft Document</string>") && p.contains("<string>PDF Document</string>"));
|
||||
fn dev_bundle_plist_is_the_release_one() {
|
||||
let p = info_plist("1.2.3-rc.1", "abc123");
|
||||
assert!(p.contains("<string>1.2.3</string>") && p.contains("<string>1.2.3-rc.1</string>"), "{p}");
|
||||
assert!(p.contains("<string>abc123</string>"));
|
||||
assert!(!p.contains('@'), "every placeholder filled");
|
||||
assert!(p.contains("<key>CFBundleDocumentTypes</key>") && p.contains("<string>pdf</string>"), "Finder opens files with it");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user