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}