fruitbasket/lib.rs
1//! fruitbasket - Framework for running Rust programs in a Mac 'app bundle' environment.
2//!
3//! fruitbasket provides two different (but related) services for helping you run your
4//! Rust binaries as native AppKit/Cocoa applications on Mac OS X:
5//!
6//! * App lifecycle and environment API - fruitbasket provides an API to initialize the
7//! AppKit application environment (NSApplication), to pump the main application loop
8//! and dispatch Apple events in a non-blocking way, to terminate the application, to
9//! access resources in the app bundle, and various other tasks frequently needed by
10//! Mac applications.
11//!
12//! * Self-bundling app 'trampoline' - fruitbasket provides a 'trampoline' to
13//! automatically bundle a standalone binary as a Mac application in a `.app` bundle
14//! at runtime. This allows access to features that require running from a bundle (
15//! such as XPC services), self-installing into the Applications folder, registering
16//! your app with the system as a document type or URL handler, and various other
17//! features that are only available to bundled apps with unique identifiers.
18//! Self-bundling and relaunching itself (the "trampoline" behavior) allows your app
19//! to get the features of app bundles, but still be launched in the standard Rust
20//! ways (such as `cargo run`).
21//!
22//! The primary goal of fruitbasket is to make it reasonably easy to develop native
23//! Mac GUI applications with the standard Apple AppKit/Cocoa/Foundation frameworks
24//! in pure Rust by pushing all of the Apple and Objective-C runtime logic into
25//! dedicated libraries, isolating the logic of a Rust binary application from the
26//! unsafe platform code. As the ecosystem of Mac libraries for Rust grows, you
27//! should be able to mix-and-match the libraries your application needs, pump the
28//! event loop with fruitbasket, and never worry about Objective-C in your application.
29//!
30//! # Getting Started
31//!
32//! You likely want to create either a [Trampoline](struct.Trampoline.html) or a
33//! [FruitApp](struct.FruitApp.html) right after your Rust application starts.
34//! If uncertain, use a `Trampoline`. You can hit very strange behavior when running
35//! Cocoa apps outside of an app bundle.
36#![deny(missing_docs)]
37
38use std::error::Error;
39use std::time::Duration;
40use std::sync::mpsc::Sender;
41
42#[cfg(any(not(target_os = "macos"), feature="dummy"))]
43use std::sync::mpsc::Receiver;
44#[cfg(any(not(target_os = "macos"), feature="dummy"))]
45use std::thread;
46
47extern crate time;
48extern crate dirs;
49
50#[cfg(all(target_os = "macos", not(feature="dummy")))]
51#[macro_use]
52extern crate objc;
53
54#[cfg(feature = "logging")]
55#[allow(unused_imports)]
56#[macro_use]
57extern crate log;
58
59#[cfg(feature = "logging")]
60extern crate log4rs;
61
62#[cfg(not(feature = "logging"))]
63#[allow(unused_macros)]
64macro_rules! info {
65 ($x:expr) => {println!($x)};
66 ($x:expr, $($arg:tt)+) => {println!($x, $($arg)+)};
67}
68
69/// Info.plist entries that have default values, but can be overridden
70///
71/// These properties are always set in the app bundle's Property List, with the
72/// default values provided here, but can be overridden by your application with
73/// the Trampoline builder's `plist_key*()` functions.
74pub const DEFAULT_PLIST: &'static [(&'static str, &'static str)] = &[
75 ("CFBundleInfoDictionaryVersion","6.0"),
76 ("CFBundlePackageType","APPL"),
77 ("CFBundleSignature","xxxx"),
78 ("LSMinimumSystemVersion","10.10.0"),
79];
80
81/// Info.plist entries that are set, and cannot be overridden
82///
83/// These properties are always set in the app bundle's Property List, based on
84/// information provided to the Trampoline builder, and cannot be overridden
85/// with the builder's `plist_key*()` functions.
86pub const FORBIDDEN_PLIST: &'static [&'static str] = & [
87 "CFBundleName",
88 "CFBundleDisplayName",
89 "CFBundleIdentifier",
90 "CFBundleExecutable",
91 "CFBundleIconFile",
92 "CFBundleVersion",
93];
94
95/// Apple kInternetEventClass constant
96#[allow(non_upper_case_globals)]
97pub const kInternetEventClass: u32 = 0x4755524c;
98/// Apple kAEGetURL constant
99#[allow(non_upper_case_globals)]
100pub const kAEGetURL: u32 = 0x4755524c;
101/// Apple keyDirectObject constant
102#[allow(non_upper_case_globals)]
103pub const keyDirectObject: u32 = 0x2d2d2d2d;
104
105#[cfg(all(target_os = "macos", not(feature="dummy")))]
106mod osx;
107
108#[cfg(all(target_os = "macos", not(feature="dummy")))]
109pub use osx::FruitApp;
110
111#[cfg(all(target_os = "macos", not(feature="dummy")))]
112pub use osx::Trampoline;
113
114#[cfg(all(target_os = "macos", not(feature="dummy")))]
115pub use osx::FruitObjcCallback;
116
117#[cfg(all(target_os = "macos", not(feature="dummy")))]
118pub use osx::FruitCallbackKey;
119
120#[cfg(all(target_os = "macos", not(feature="dummy")))]
121pub use osx::parse_url_event;
122
123#[cfg(all(target_os = "macos", not(feature="dummy")))]
124pub use osx::nsstring_to_string;
125
126#[cfg(any(not(target_os = "macos"), feature="dummy"))]
127/// Docs in OS X build.
128pub enum FruitCallbackKey {
129 /// Docs in OS X build.
130 Method(&'static str),
131 /// Docs in OS X build.
132 Object(*mut u64),
133}
134
135#[cfg(any(not(target_os = "macos"), feature="dummy"))]
136/// Docs in OS X build.
137pub type FruitObjcCallback = Box<dyn Fn(*mut u64)>;
138
139/// Main interface for controlling and interacting with the AppKit app
140///
141/// Dummy implementation for non-OSX platforms. See OS X build for proper
142/// documentation.
143#[cfg(any(not(target_os = "macos"), feature="dummy"))]
144pub struct FruitApp {
145 tx: Sender<()>,
146 rx: Receiver<()>,
147}
148#[cfg(any(not(target_os = "macos"), feature="dummy"))]
149impl FruitApp {
150 /// Docs in OS X build.
151 pub fn new() -> FruitApp {
152 use std::sync::mpsc::channel;
153 let (tx,rx) = channel();
154 FruitApp{ tx: tx, rx: rx}
155 }
156 /// Docs in OS X build.
157 pub fn register_callback(&mut self, _key: FruitCallbackKey, _cb: FruitObjcCallback) {}
158 /// Docs in OS X build.
159 pub fn register_apple_event(&mut self, _class: u32, _id: u32) {}
160 /// Docs in OS X build.
161 pub fn set_activation_policy(&self, _policy: ActivationPolicy) {}
162 /// Docs in OS X build.
163 pub fn terminate(exit_code: i32) {
164 std::process::exit(exit_code);
165 }
166 /// Docs in OS X build.
167 pub fn stop(stopper: &FruitStopper) {
168 stopper.stop();
169 }
170 /// Docs in OS X build.
171 pub fn run(&mut self, period: RunPeriod) -> Result<(),()> {
172 let start = time::now_utc().to_timespec();
173 loop {
174 if self.rx.try_recv().is_ok() {
175 return Err(());
176 }
177 if period == RunPeriod::Once {
178 break;
179 }
180 thread::sleep(Duration::from_millis(500));
181 if let RunPeriod::Time(t) = period {
182 let now = time::now_utc().to_timespec();
183 if now >= start + time::Duration::from_std(t).unwrap() {
184 break;
185 }
186 }
187 }
188 Ok(())
189 }
190 /// Docs in OS X build.
191 pub fn stopper(&self) -> FruitStopper {
192 FruitStopper { tx: self.tx.clone() }
193 }
194 /// Docs in OS X build.
195 pub fn bundled_resource_path(_name: &str, _extension: &str) -> Option<String> { None }
196}
197
198#[cfg(any(not(target_os = "macos"), feature="dummy"))]
199/// Docs in OS X build.
200pub fn parse_url_event(_event: *mut u64) -> String { "".into() }
201
202#[cfg(any(not(target_os = "macos"), feature = "dummy"))]
203/// Docs in OS X build.
204pub fn nsstring_to_string(_nsstring: *mut u64) -> String {
205 "".into()
206}
207
208/// API to move the executable into a Mac app bundle and relaunch (if necessary)
209///
210/// Dummy implementation for non-OSX platforms. See OS X build for proper
211/// documentation.
212#[cfg(any(not(target_os = "macos"), feature="dummy"))]
213pub struct Trampoline {}
214#[cfg(any(not(target_os = "macos"), feature="dummy"))]
215impl Trampoline {
216 /// Docs in OS X build.
217 pub fn new(_name: &str, _exe: &str, _ident: &str) -> Trampoline { Trampoline {} }
218 /// Docs in OS X build.
219 pub fn name(&mut self, _name: &str) -> &mut Self { self }
220 /// Docs in OS X build.
221 pub fn exe(&mut self, _exe: &str) -> &mut Self { self }
222 /// Docs in OS X build.
223 pub fn ident(&mut self, _ident: &str) -> &mut Self { self }
224 /// Docs in OS X build.
225 pub fn icon(&mut self, _icon: &str) -> &mut Self { self }
226 /// Docs in OS X build.
227 pub fn version(&mut self, _version: &str) -> &mut Self { self }
228 /// Docs in OS X build.
229 pub fn plist_key(&mut self, _key: &str, _value: &str) -> &mut Self { self }
230 /// Docs in OS X build.
231 pub fn plist_keys(&mut self, _pairs: &Vec<(&str,&str)>) -> &mut Self { self }
232 /// Docs in OS X build.
233 pub fn retina(&mut self, _doit: bool) -> &mut Self { self }
234 /// Docs in OS X build.
235 pub fn plist_raw_string(&mut self, _s: String) -> &mut Self { self }
236 /// Docs in OS X build.
237 pub fn resource(&mut self, _file: &str) -> &mut Self { self }
238 /// Docs in OS X build.
239 pub fn resources(&mut self, _files: &Vec<&str>) -> &mut Self{ self }
240 /// Docs in OS X build.
241 pub fn build(&mut self, dir: InstallDir) -> Result<FruitApp, FruitError> {
242 self.self_bundle(dir)?;
243 unreachable!()
244 }
245 /// Docs in OS X build.
246 pub fn self_bundle(&mut self, _dir: InstallDir) -> Result<(), FruitError> {
247 Err(FruitError::UnsupportedPlatform("fruitbasket disabled or not supported on this platform.".to_string()))
248 }
249 /// Docs in OS X build.
250 pub fn is_bundled() -> bool { false }
251}
252
253/// Options for how long to run the event loop on each call
254#[derive(PartialEq)]
255pub enum RunPeriod {
256 /// Run event loop once and return
257 Once,
258 /// Run event loop forever, never returning and blocking the main thread
259 Forever,
260 /// Run event loop at least the specified length of time
261 Time(Duration),
262}
263
264/// Policies controlling how a Mac application's UI is interacted with
265pub enum ActivationPolicy {
266 /// Appears in the Dock and menu bar and can have an interactive UI with windows
267 Regular,
268 /// Does not appear in Dock or menu bar, but may create windows
269 Accessory,
270 /// Does not appear in Dock or menu bar, may not create windows (background-only)
271 Prohibited,
272}
273
274/// Class for errors generated by fruitbasket. Dereferences to a String.
275#[derive(Debug)]
276pub enum FruitError {
277 /// fruitbasket doesn't run on this platform (safe to ignore)
278 UnsupportedPlatform(String),
279 /// Disk I/O errors: failed to write app bundle to disk
280 IOError(String),
281 /// Any other unclassified error
282 GeneralError(String),
283}
284
285impl std::fmt::Display for FruitError {
286 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
287 write!(f, "{:?}", self)
288 }
289}
290impl From<std::io::Error> for FruitError {
291 fn from(error: std::io::Error) -> Self {
292 FruitError::IOError(error.to_string())
293 }
294}
295impl Error for FruitError {
296 fn description(&self) -> &str {
297 "Hmm"
298 }
299 fn cause(&self) -> Option<&dyn Error> {
300 None
301 }
302}
303
304/// An opaque, thread-safe object that can interrupt the run loop.
305///
306/// An object that is safe to pass across thread boundaries (i.e. it implements
307/// Send and Sync), and can be used to interrupt and stop the run loop, even
308/// when running in 'Forever' mode. It can be Cloned infinite times and used
309/// from any thread.
310#[derive(Clone)]
311pub struct FruitStopper {
312 tx: Sender<()>,
313}
314impl FruitStopper {
315 /// Stop the run loop on the `FruitApp` instance that created this object
316 ///
317 /// This is equivalent to passing the object to [FruitApp::stop](FruitApp::stop). See it
318 /// for more documentation.
319 pub fn stop(&self) {
320 let _ = self.tx.send(());
321 }
322}
323
324/// Options for where to save generated app bundle
325pub enum InstallDir {
326 /// Store in a system-defined temporary directory
327 Temp,
328 /// Store in the system-wide Application directory (all users)
329 SystemApplications,
330 /// Store in the user-specific Application directory (current user)
331 UserApplications,
332 /// Store in a custom directory, specified as a String
333 Custom(String),
334}
335
336/// Options for where to save logging output generated by fruitbasket
337pub enum LogDir {
338 /// User's home directory
339 Home,
340 /// Temporary directory (as specified by OS)
341 Temp,
342 /// Custom location, provided as a String
343 Custom(String),
344}
345
346/// Enable logging to rolling log files with Rust `log` library
347///
348/// Requires the 'logging' feature to be specified at compile time.
349///
350/// This is a helper utility for configuring the Rust `log` and `log4rs`
351/// libraries to redirect the `log` macros (`info!()`, `warn!()`, `err!()`, etc)
352/// to both stdout and a rotating log file on disk.
353///
354/// If you specify the Home directory with a log named ".fruit.log" and a
355/// backup count of 3, eventually you will end up with the files `~/.fruit.log`,
356/// `~/.fruit.log.1`, `~/.fruit.log.2`, and `~/.fruit.log.3`
357///
358/// The maximum disk space used by the log files, in megabytes, will be:
359///
360/// `(backup_count + 1) * max_size_mb`
361///
362/// # Arguments
363///
364/// `filename` - Filename for the log file, *without* path
365///
366/// `dir` - Directory to save log files in. This is provided as an enum,
367/// `LogDir`, which offers some standard logging directories, or allows
368/// specification of any custom directory.
369///
370/// `max_size_mb` - Max size (in megabytes) of the log file before it is rolled
371/// into an archive file in the same directory.
372///
373/// `backup_count` - Number of archived log files to keep before deleting old
374/// logs.
375///
376/// # Returns
377///
378/// Full path to opened log file on disk
379#[cfg(feature = "logging")]
380pub fn create_logger(filename: &str,
381 dir: LogDir,
382 max_size_mb: u32,
383 backup_count: u32) -> Result<String, String> {
384 use log::LevelFilter;
385 use self::log4rs::append::console::ConsoleAppender;
386 use self::log4rs::append::rolling_file::RollingFileAppender;
387 use self::log4rs::append::rolling_file::policy::compound::CompoundPolicy;
388 use self::log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
389 use self::log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
390 use self::log4rs::encode::pattern::PatternEncoder;
391 use self::log4rs::config::{Appender, Config, Logger, Root};
392
393 let log_path = match dir {
394 LogDir::Home => format!("{}/{}", dirs::home_dir().unwrap().display(), filename),
395 LogDir::Temp => format!("{}/{}", std::env::temp_dir().display(), filename),
396 LogDir::Custom(s) => format!("{}/{}", s, filename),
397 };
398 let stdout = ConsoleAppender::builder()
399 .encoder(Box::new(PatternEncoder::new("{m}{n}")))
400 .build();
401 let trigger = Box::new(SizeTrigger::new(1024*1024*max_size_mb as u64));
402 let roller = Box::new(FixedWindowRoller::builder()
403 .build(&format!("{}.{{}}", log_path), backup_count).unwrap());
404 let policy = Box::new(CompoundPolicy::new(trigger, roller));
405 let rolling = RollingFileAppender::builder()
406 .build(&log_path, policy)
407 .unwrap();
408
409 let config = Config::builder()
410 .appender(Appender::builder().build("stdout", Box::new(stdout)))
411 .appender(Appender::builder().build("requests", Box::new(rolling)))
412 .logger(Logger::builder().build("app::backend::db", LevelFilter::Info))
413 .logger(Logger::builder()
414 .appender("requests")
415 .additive(false)
416 .build("app::requests", LevelFilter::Info))
417 .build(Root::builder().appender("stdout").appender("requests").build(LevelFilter::Info))
418 .unwrap();
419 match log4rs::init_config(config) {
420 Ok(_) => Ok(log_path),
421 Err(e) => Err(e.to_string()),
422 }
423}
424/// Enable logging to rolling log files with Rust `log` library
425///
426/// Requires the 'logging' feature to be specified at compile time.
427///
428/// This is a helper utility for configuring the Rust `log` and `log4rs`
429/// libraries to redirect the `log` macros (`info!()`, `warn!()`, `error!()`, etc)
430/// to both stdout and a rotating log file on disk.
431///
432/// If you specify the Home directory with a log named ".fruit.log" and a
433/// backup count of 3, eventually you will end up with the files `~/.fruit.log`,
434/// `~/.fruit.log.1`, `~/.fruit.log.2`, and `~/.fruit.log.3`
435///
436/// The maximum disk space used by the log files, in megabytes, will be:
437///
438/// `(backup_count + 1) * max_size_mb`
439///
440/// # Arguments
441///
442/// `filename` - Filename for the log file, *without* path
443///
444/// `dir` - Directory to save log files in. This is provided as an enum,
445/// `LogDir`, which offers some standard logging directories, or allows
446/// specification of any custom directory.
447///
448/// `max_size_mb` - Max size (in megabytes) of the log file before it is rolled
449/// into an archive file in the same directory.
450///
451/// `backup_count` - Number of archived log files to keep before deleting old
452/// logs.
453///
454/// # Returns
455///
456/// Full path to opened log file on disk
457#[cfg(not(feature = "logging"))]
458pub fn create_logger(_filename: &str,
459 _dir: LogDir,
460 _max_size_mb: u32,
461 _backup_count: u32) -> Result<String, FruitError> {
462 Err(FruitError::GeneralError("Must recompile with 'logging' feature to use logger.".to_string()))
463}