├── .gitignore ├── examples ├── icon.png ├── example.rs └── register_url.rs ├── .travis.yml ├── Cargo.toml ├── README.md ├── LICENSE └── src ├── lib.rs └── osx.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | *~ 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /examples/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmekon/fruitbasket/HEAD/examples/icon.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: rust 3 | rust: 4 | - stable 5 | - beta 6 | - nightly 7 | os: 8 | - osx 9 | - linux 10 | matrix: 11 | allow_failures: 12 | - rust: nightly 13 | # Only build direct changes to master branch. PRs still built. 14 | branches: 15 | only: 16 | - "master" 17 | osx_image: xcode8.3 18 | install: 19 | - curl https://static.rust-lang.org/rustup.sh | 20 | sh -s -- --prefix=$HOME/rust 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fruitbasket" 3 | version = "0.10.0" 4 | authors = ["Trevor Bentley "] 5 | description = "Framework for running Rust programs in a Mac 'app bundle' environment." 6 | keywords = ["mac", "osx", "bundle", "cocoa", "appkit"] 7 | categories = ["api-bindings"] 8 | homepage = "https://github.com/mrmekon/fruitbasket" 9 | repository = "https://github.com/mrmekon/fruitbasket" 10 | documentation = "https://mrmekon.github.io/fruitbasket/fruitbasket/" 11 | license = "Apache-2.0" 12 | readme = "README.md" 13 | 14 | [features] 15 | logging = ["log", "log4rs"] 16 | dummy = [] 17 | 18 | [dependencies] 19 | time = "0.1" 20 | log = {version = "0.4", optional = true, default-features = false, features = ["std"] } 21 | dirs = "4" 22 | 23 | [dependencies.log4rs] 24 | version = "0.8" 25 | optional = true 26 | default-features = false 27 | features = ["console_appender","rolling_file_appender", "compound_policy", "fixed_window_roller", "size_trigger"] 28 | 29 | [dev-dependencies] 30 | log = "0.4" 31 | 32 | [target."cfg(target_os = \"macos\")".dependencies] 33 | objc-foundation = "0.1" 34 | objc_id = "0.1" 35 | 36 | [target."cfg(target_os = \"macos\")".dependencies.objc] 37 | version = "0.2" 38 | features = ["exception"] 39 | 40 | [package.metadata.release] 41 | sign-commit = false 42 | pre-release-commit-message = "Release {{version}}" 43 | dev-version-ext = "rc" 44 | tag-message = "Release {{version}}" 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fruitbasket - Mac app Framework for Rust 2 | 3 | [![Build Status](https://travis-ci.org/mrmekon/fruitbasket.svg?branch=master)](https://travis-ci.org/mrmekon/fruitbasket) 4 | [![Crates.io Version](https://img.shields.io/crates/v/fruitbasket.svg)](https://crates.io/crates/fruitbasket) 5 | 6 | fruitbasket provides two different (but related) services for helping you run your 7 | Rust binaries as native AppKit/Cocoa applications on Mac OS X: 8 | 9 | * App lifecycle and environment API - fruitbasket provides an API to initialize the 10 | AppKit application environment (NSApplication), to pump the main application loop 11 | and dispatch Apple events in a non-blocking way, to terminate the application, to 12 | access resources in the app bundle, and various other tasks frequently needed by 13 | Mac applications. 14 | 15 | * Self-bundling app 'trampoline' - fruitbasket provides a 'trampoline' to 16 | automatically bundle a standalone binary as a Mac application in a `.app` bundle 17 | at runtime. This allows access to features that require running from a bundle ( 18 | such as XPC services), self-installing into the Applications folder, registering 19 | your app with the system as a document type or URL handler, and various other 20 | features that are only available to bundled apps with unique identifiers. 21 | Self-bundling and relaunching itself (the "trampoline" behavior) allows your app 22 | to get the features of app bundles, but still be launched in the standard Rust 23 | ways (such as `cargo run`). 24 | 25 | The primary goal of fruitbasket is to make it reasonably easy to develop native 26 | Mac GUI applications with the standard Apple AppKit/Cocoa/Foundation frameworks 27 | in pure Rust by pushing all of the Apple and Objective-C runtime logic into 28 | dedicated libraries, isolating the logic of a Rust binary application from the 29 | unsafe platform code. As the ecosystem of Mac libraries for Rust grows, you 30 | should be able to mix-and-match the libraries your application needs, pump the 31 | event loop with fruitbasket, and never worry about Objective-C in your application. 32 | 33 | See the `examples/` dir for demo usage. 34 | 35 | ## Documentation 36 | 37 | [API documentation](https://mrmekon.github.io/fruitbasket/fruitbasket/) 38 | -------------------------------------------------------------------------------- /examples/example.rs: -------------------------------------------------------------------------------- 1 | extern crate fruitbasket; 2 | use fruitbasket::ActivationPolicy; 3 | use fruitbasket::Trampoline; 4 | use fruitbasket::FruitApp; 5 | use fruitbasket::InstallDir; 6 | use fruitbasket::RunPeriod; 7 | use fruitbasket::FruitError; 8 | use std::time::Duration; 9 | use std::path::PathBuf; 10 | 11 | #[macro_use] 12 | extern crate log; 13 | 14 | fn main() { 15 | let _ = fruitbasket::create_logger(".fruitbasket.log", fruitbasket::LogDir::Home, 5, 2).unwrap(); 16 | 17 | // Find the icon file from the Cargo project dir 18 | let icon = PathBuf::from(env!("CARGO_MANIFEST_DIR")) 19 | .join("examples").join("icon.png"); 20 | 21 | // Re-launch self in an app bundle if not already running from one. 22 | info!("Executable must run from App bundle. Let's try:"); 23 | let mut app = match Trampoline::new("fruitbasket", "fruitbasket", "com.trevorbentley.fruitbasket") 24 | .version("2.1.3") 25 | .icon("fruitbasket.icns") 26 | .plist_key("CFBundleSpokenName","\"fruit basket\"") 27 | .plist_keys(&vec![ 28 | ("LSMinimumSystemVersion", "10.12.0"), 29 | ("LSBackgroundOnly", "1"), 30 | ]) 31 | .resource(icon.to_str().unwrap()) 32 | .build(InstallDir::Temp) { 33 | Err(FruitError::UnsupportedPlatform(_)) => { 34 | info!("This is not a Mac. App bundling is not supported."); 35 | info!("It is still safe to use FruitApp::new(), though the dummy app will do nothing."); 36 | FruitApp::new() 37 | }, 38 | Err(FruitError::IOError(e)) => { 39 | info!("IO error! {}", e); 40 | std::process::exit(1); 41 | }, 42 | Err(FruitError::GeneralError(e)) => { 43 | info!("General error! {}", e); 44 | std::process::exit(1); 45 | }, 46 | Ok(app) => app, 47 | }; 48 | 49 | // App is guaranteed to be running in a bundle now! 50 | 51 | // Make it a regular app in the dock. 52 | // Note: Because 'LSBackgroundOnly' is set to true in the Info.plist, the 53 | // app will launch backgrounded and will not take focus. If we only did 54 | // that, the app would stay in 'Prohibited' mode and would not create a dock 55 | // icon. By overriding the activation policy now, it will stay background 56 | // but create the Dock and menu bar entries. This basically implements a 57 | // "pop-under" behavior. 58 | app.set_activation_policy(ActivationPolicy::Regular); 59 | 60 | // Give it a bit of time for the launching process to quit, to prove that 61 | // the bundled process is not a dependent child of the un-bundled process. 62 | info!("Spawned process started. Sleeping for a bit..."); 63 | let _ = app.run(RunPeriod::Time(Duration::from_secs(1))); 64 | 65 | // Demonstrate stopping an infinite run loop from another thread. 66 | let stopper = app.stopper(); 67 | let _ = std::thread::spawn(move || { 68 | std::thread::sleep(Duration::from_secs(4)); 69 | info!("Stopping run loop."); 70 | fruitbasket::FruitApp::stop(&stopper); 71 | }); 72 | 73 | // Run 'forever', until the other thread interrupts. 74 | info!("Spawned process running!"); 75 | let _ = app.run(RunPeriod::Forever); 76 | info!("Run loop stopped from other thread."); 77 | 78 | // Find the icon we stored in the bundle 79 | let icon = fruitbasket::FruitApp::bundled_resource_path("icon", "png"); 80 | info!("Bundled icon: {}", icon.unwrap_or("MISSING!".to_string())); 81 | 82 | // Cleanly terminate 83 | fruitbasket::FruitApp::terminate(0); 84 | info!("This will not print."); 85 | } 86 | -------------------------------------------------------------------------------- /examples/register_url.rs: -------------------------------------------------------------------------------- 1 | /// Example that launches as Mac App with custom URL scheme handler 2 | /// 3 | /// In one terminal, build and run: 4 | /// 5 | /// $ cargo build --features=logging --examples 6 | /// $ ./target/debug/examples/register_url && tail -f ~/.fruitbasket_register_url.log 7 | /// 8 | /// In a second terminal, open custom URL: 9 | /// 10 | /// $ open fruitbasket://test 11 | /// 12 | /// Log output will show that the example has received and printed the custom URL. 13 | /// 14 | extern crate fruitbasket; 15 | use fruitbasket::ActivationPolicy; 16 | use fruitbasket::Trampoline; 17 | use fruitbasket::FruitApp; 18 | use fruitbasket::InstallDir; 19 | use fruitbasket::RunPeriod; 20 | use fruitbasket::FruitError; 21 | use fruitbasket::FruitCallbackKey; 22 | use std::path::PathBuf; 23 | 24 | #[macro_use] 25 | extern crate log; 26 | 27 | fn main() { 28 | let _ = fruitbasket::create_logger(".fruitbasket_register_url.log", fruitbasket::LogDir::Home, 5, 2).unwrap(); 29 | 30 | // Find the icon file from the Cargo project dir 31 | let icon = PathBuf::from(env!("CARGO_MANIFEST_DIR")) 32 | .join("examples").join("icon.png"); 33 | 34 | // Re-launch self in an app bundle if not already running from one. 35 | info!("Executable must run from App bundle. Let's try:"); 36 | let mut app = match Trampoline::new("fruitbasket_register_url", "fruitbasket", "com.trevorbentley.fruitbasket_register_url") 37 | .version("2.1.3") 38 | .icon("fruitbasket.icns") 39 | .plist_key("CFBundleSpokenName","\"fruit basket\"") 40 | .plist_keys(&vec![ 41 | ("LSMinimumSystemVersion", "10.12.0"), 42 | ("LSBackgroundOnly", "1"), 43 | ]) 44 | // Register "fruitbasket://" and "fbasket://" URL schemes in Info.plist 45 | .plist_raw_string(" 46 | CFBundleURLTypes = ( { 47 | CFBundleTypeRole = \"Viewer\"; 48 | CFBundleURLName = \"Fruitbasket Example URL\"; 49 | CFBundleURLSchemes = (\"fruitbasket\", \"fbasket\"); 50 | } );\n".into()) 51 | .resource(icon.to_str().unwrap()) 52 | .build(InstallDir::Temp) { 53 | Err(FruitError::UnsupportedPlatform(_)) => { 54 | info!("This is not a Mac. App bundling is not supported."); 55 | info!("It is still safe to use FruitApp::new(), though the dummy app will do nothing."); 56 | FruitApp::new() 57 | }, 58 | Err(FruitError::IOError(e)) => { 59 | info!("IO error! {}", e); 60 | std::process::exit(1); 61 | }, 62 | Err(FruitError::GeneralError(e)) => { 63 | info!("General error! {}", e); 64 | std::process::exit(1); 65 | }, 66 | Ok(app) => app, 67 | }; 68 | 69 | // App is guaranteed to be running in a bundle now! 70 | 71 | // Make it a regular app in the dock. 72 | // Note: Because 'LSBackgroundOnly' is set to true in the Info.plist, the 73 | // app will launch backgrounded and will not take focus. If we only did 74 | // that, the app would stay in 'Prohibited' mode and would not create a dock 75 | // icon. By overriding the activation policy now, it will stay background 76 | // but create the Dock and menu bar entries. This basically implements a 77 | // "pop-under" behavior. 78 | app.set_activation_policy(ActivationPolicy::Regular); 79 | 80 | // Register a callback for when the ObjC application finishes launching 81 | let stopper = app.stopper(); 82 | app.register_callback(FruitCallbackKey::Method("applicationWillFinishLaunching:"), 83 | Box::new(move |_event| { 84 | info!("applicationDidFinishLaunching."); 85 | stopper.stop(); 86 | })); 87 | 88 | // Run until callback is called 89 | info!("Spawned process started. Run until applicationDidFinishLaunching."); 90 | let _ = app.run(RunPeriod::Forever); 91 | 92 | info!("Application launched. Registering URL callbacks."); 93 | // Register a callback to get receive custom URL schemes from any Mac program 94 | app.register_apple_event(fruitbasket::kInternetEventClass, fruitbasket::kAEGetURL); 95 | let stopper = app.stopper(); 96 | app.register_callback(FruitCallbackKey::Method("handleEvent:withReplyEvent:"), 97 | Box::new(move |event| { 98 | // Event is a raw NSAppleEventDescriptor. 99 | // Fruitbasket has a parser for URLs. Call that to get the URL: 100 | let url: String = fruitbasket::parse_url_event(event); 101 | info!("Received URL: {}", url); 102 | stopper.stop(); 103 | })); 104 | 105 | let stopper = app.stopper(); 106 | app.register_callback( 107 | FruitCallbackKey::Method("application:openFile:"), 108 | Box::new(move |file| { 109 | // File is a raw NSString. 110 | // Fruitbasket has a converter to Rust String: 111 | let file: String = fruitbasket::nsstring_to_string(file); 112 | info!("Received file: {}", file); 113 | stopper.stop(); 114 | }), 115 | ); 116 | 117 | // Run 'forever', until one of the URL or file callbacks fire 118 | info!("Spawned process running!"); 119 | let _ = app.run(RunPeriod::Forever); 120 | info!("Run loop stopped after URL callback."); 121 | 122 | // Cleanly terminate 123 | fruitbasket::FruitApp::terminate(0); 124 | info!("This will not print."); 125 | } 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/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 | 38 | use std::error::Error; 39 | use std::time::Duration; 40 | use std::sync::mpsc::Sender; 41 | 42 | #[cfg(any(not(target_os = "macos"), feature="dummy"))] 43 | use std::sync::mpsc::Receiver; 44 | #[cfg(any(not(target_os = "macos"), feature="dummy"))] 45 | use std::thread; 46 | 47 | extern crate time; 48 | extern crate dirs; 49 | 50 | #[cfg(all(target_os = "macos", not(feature="dummy")))] 51 | #[macro_use] 52 | extern crate objc; 53 | 54 | #[cfg(feature = "logging")] 55 | #[allow(unused_imports)] 56 | #[macro_use] 57 | extern crate log; 58 | 59 | #[cfg(feature = "logging")] 60 | extern crate log4rs; 61 | 62 | #[cfg(not(feature = "logging"))] 63 | #[allow(unused_macros)] 64 | macro_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. 74 | pub 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. 86 | pub 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)] 97 | pub const kInternetEventClass: u32 = 0x4755524c; 98 | /// Apple kAEGetURL constant 99 | #[allow(non_upper_case_globals)] 100 | pub const kAEGetURL: u32 = 0x4755524c; 101 | /// Apple keyDirectObject constant 102 | #[allow(non_upper_case_globals)] 103 | pub const keyDirectObject: u32 = 0x2d2d2d2d; 104 | 105 | #[cfg(all(target_os = "macos", not(feature="dummy")))] 106 | mod osx; 107 | 108 | #[cfg(all(target_os = "macos", not(feature="dummy")))] 109 | pub use osx::FruitApp; 110 | 111 | #[cfg(all(target_os = "macos", not(feature="dummy")))] 112 | pub use osx::Trampoline; 113 | 114 | #[cfg(all(target_os = "macos", not(feature="dummy")))] 115 | pub use osx::FruitObjcCallback; 116 | 117 | #[cfg(all(target_os = "macos", not(feature="dummy")))] 118 | pub use osx::FruitCallbackKey; 119 | 120 | #[cfg(all(target_os = "macos", not(feature="dummy")))] 121 | pub use osx::parse_url_event; 122 | 123 | #[cfg(all(target_os = "macos", not(feature="dummy")))] 124 | pub use osx::nsstring_to_string; 125 | 126 | #[cfg(any(not(target_os = "macos"), feature="dummy"))] 127 | /// Docs in OS X build. 128 | pub 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. 137 | pub type FruitObjcCallback = Box; 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"))] 144 | pub struct FruitApp { 145 | tx: Sender<()>, 146 | rx: Receiver<()>, 147 | } 148 | #[cfg(any(not(target_os = "macos"), feature="dummy"))] 149 | impl 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 { None } 196 | } 197 | 198 | #[cfg(any(not(target_os = "macos"), feature="dummy"))] 199 | /// Docs in OS X build. 200 | pub 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. 204 | pub 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"))] 213 | pub struct Trampoline {} 214 | #[cfg(any(not(target_os = "macos"), feature="dummy"))] 215 | impl 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 { 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)] 255 | pub 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 265 | pub 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)] 276 | pub 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 | 285 | impl std::fmt::Display for FruitError { 286 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 287 | write!(f, "{:?}", self) 288 | } 289 | } 290 | impl From for FruitError { 291 | fn from(error: std::io::Error) -> Self { 292 | FruitError::IOError(error.to_string()) 293 | } 294 | } 295 | impl 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)] 311 | pub struct FruitStopper { 312 | tx: Sender<()>, 313 | } 314 | impl 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 325 | pub 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 337 | pub 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")] 380 | pub fn create_logger(filename: &str, 381 | dir: LogDir, 382 | max_size_mb: u32, 383 | backup_count: u32) -> Result { 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"))] 458 | pub fn create_logger(_filename: &str, 459 | _dir: LogDir, 460 | _max_size_mb: u32, 461 | _backup_count: u32) -> Result { 462 | Err(FruitError::GeneralError("Must recompile with 'logging' feature to use logger.".to_string())) 463 | } 464 | -------------------------------------------------------------------------------- /src/osx.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 | 38 | // Temporarily (mmmmhmm...) disable deprecated function warnings, because objc macros 39 | // throw tons of them in rustc 1.34-nightly when initializing atomic uints. 40 | #![allow(deprecated)] 41 | 42 | use std; 43 | use std::thread; 44 | use std::time::Duration; 45 | use std::path::Path; 46 | use std::path::PathBuf; 47 | use std::io::Write; 48 | use std::cell::Cell; 49 | use std::sync::mpsc::channel; 50 | use std::sync::mpsc::Receiver; 51 | use std::sync::mpsc::Sender; 52 | use std::collections::HashMap; 53 | 54 | use super::FruitError; 55 | use super::ActivationPolicy; 56 | use super::RunPeriod; 57 | use super::InstallDir; 58 | use super::FruitStopper; 59 | use super::DEFAULT_PLIST; 60 | use super::FORBIDDEN_PLIST; 61 | 62 | extern crate time; 63 | 64 | extern crate dirs; 65 | 66 | extern crate objc; 67 | use objc::runtime::Object; 68 | use objc::runtime::Class; 69 | 70 | extern crate objc_id; 71 | use self::objc_id::Id; 72 | use self::objc_id::WeakId; 73 | use self::objc_id::Shared; 74 | 75 | extern crate objc_foundation; 76 | use std::sync::{Once, ONCE_INIT}; 77 | use objc::Message; 78 | use objc::declare::ClassDecl; 79 | use objc::runtime::{Sel}; 80 | use self::objc_foundation::{INSObject, NSObject}; 81 | 82 | 83 | #[allow(non_upper_case_globals)] 84 | const nil: *mut Object = 0 as *mut Object; 85 | 86 | #[link(name = "Foundation", kind = "framework")] 87 | #[link(name = "CoreFoundation", kind = "framework")] 88 | #[link(name = "ApplicationServices", kind = "framework")] 89 | #[link(name = "AppKit", kind = "framework")] 90 | extern {} 91 | 92 | /// Main interface for controlling and interacting with the AppKit app 93 | /// 94 | /// `FruitApp` is an instance of an AppKit app, equivalent to (and containing) 95 | /// the NSApplication singleton that is responsible for the app's lifecycle 96 | /// and participation in the Mac app ecosystem. 97 | /// 98 | /// You must initialize a single instance of FruitApp before using any Apple 99 | /// frameworks, and after creating it you must regularly pump its event loop. 100 | /// 101 | /// You must follow all of the standard requirements for NSApplication. Most 102 | /// notably: FruitApp **must** be created on your app's main thread, and **must** 103 | /// be pumped from the same main thread. Doing otherwise angers the beast. 104 | /// 105 | /// An application does *not* need to be in a Mac app bundle to run, so this can 106 | /// be created in any application with [FruitApp::new](FruitApp::new). However, many Apple 107 | /// frameworks *do* require the application to be running from a bundle, so you 108 | /// may want to consider creating your FruitApp instance from the [Trampoline](Trampoline) 109 | /// struct's builder instead. 110 | /// 111 | pub struct FruitApp<'a> { 112 | app: *mut Object, 113 | pool: Cell<*mut Object>, 114 | run_count: Cell, 115 | run_mode: *mut Object, 116 | tx: Sender<()>, 117 | rx: Receiver<()>, 118 | objc: Box>, 119 | } 120 | 121 | /// A boxed Fn type for receiving Rust callbacks from ObjC events 122 | pub type FruitObjcCallback<'a> = Box; 123 | 124 | /// Key into the ObjC callback hash map 125 | /// 126 | /// You can register to receive callbacks from ObjectiveC based on these keys. 127 | /// 128 | /// Callbacks that are not tied to objects can be registered with static 129 | /// selector strings. For instance, if your app has registered itself as a URL 130 | /// handler, you would use: 131 | /// FruitCallbackKey::Method("handleEvent:withReplyEvent:") 132 | /// 133 | /// Other pre-defined selectors are: 134 | /// FruitCallbackKey::Method("applicationWillFinishlaunching:") 135 | /// FruitCallbackKey::Method("applicationDidFinishlaunching:") 136 | /// 137 | /// The Object variant is currently unused, and reserved for the future. 138 | /// If the callback will be from a particular object, you use the Object type 139 | /// with the ObjC object included. For example, if you want to register for 140 | /// callbacks from a particular NSButton instance, you would add it to the 141 | /// callback map with: 142 | /// let button1: *mut Object = ; 143 | /// app.register_callback(FruitCallbackKey::Object(button), 144 | /// Box::new(|button1| { 145 | /// println!("got callback from button1, address: {:x}", button1 as u64); 146 | /// })); 147 | /// 148 | #[derive(PartialEq, Eq, Hash)] 149 | pub enum FruitCallbackKey { 150 | /// A callback tied to a generic selector 151 | Method(&'static str), 152 | /// A callback from a specific object instance 153 | Object(*mut Object), 154 | } 155 | 156 | /// Rust class for wrapping Objective-C callback class 157 | /// 158 | /// There is one Objective-C object, implemented in Rust but registered with and 159 | /// owned by the Objective-C runtime, which handles ObjC callbacks such as those 160 | /// for the NSApplication delegate. This is a native Rust class that wraps the 161 | /// ObjC object. 162 | /// 163 | /// There should be exactly one of this object, and it must be stored on the 164 | /// heap (i.e. in a Box). This is because the ObjC object calls into this class 165 | /// via raw function pointers, and its address must not change. 166 | /// 167 | struct ObjcWrapper<'a> { 168 | objc: Id, 169 | map: HashMap>, 170 | } 171 | 172 | impl<'a> ObjcWrapper<'a> { 173 | fn take(&mut self) -> Id { 174 | let weak = WeakId::new(&self.objc); 175 | weak.load().unwrap() 176 | } 177 | } 178 | 179 | /// API to move the executable into a Mac app bundle and relaunch (if necessary) 180 | /// 181 | /// `Trampoline` is a builder pattern for creating a `FruitApp` application 182 | /// instance that is guaranteed to be running inside a Mac app bundle. See the 183 | /// module documentation for why this is often important. 184 | /// 185 | /// If the currently running process is already in an app bundle, Trampoline 186 | /// does nothing and is equivalent to calling [FruitApp::new](FruitApp::new). 187 | /// 188 | /// The builder takes a variety of information that is required for creating a 189 | /// Mac app (notably: app name, executable name, unique identifier), as well 190 | /// as optional metadata to describe your app and its interactions to the OS, 191 | /// and optional file resources to bundle with it. It creates an app bundle, 192 | /// either in an install path of your choosing or in a temporary directory, 193 | /// launches the bundled app, and terminates the non-bundled binary. 194 | /// 195 | /// Care should be taken to call this very early in your application, since any 196 | /// work done prior to this will be discarded when the app is relaunched. Your 197 | /// program should also gracefully support relaunching from a different directory. 198 | /// Take care not to perform any actions that would prevent relaunching, such as 199 | /// claiming locks, until after the trampoline. 200 | /// 201 | #[derive(Default)] 202 | pub struct Trampoline { 203 | name: String, 204 | exe: String, 205 | ident: String, 206 | icon: String, 207 | version: String, 208 | keys: Vec<(String,String)>, 209 | plist_raw_strings: Vec, 210 | resources: Vec, 211 | hidpi: bool, 212 | } 213 | 214 | impl Trampoline { 215 | /// Creates a new Trampoline builder to build a Mac app bundle 216 | /// 217 | /// This creates a new Trampoline builder, which takes the information 218 | /// required to construct a Mac `.app` bundle. If your application 219 | /// is already running in a bundle, the builder does not create a bundle 220 | /// and simply returns a newly constructed `FruitApp` object with the Mac 221 | /// application environment initialized. If your application is not in 222 | /// a bundle, a new bundle is created and launched, and your program's 223 | /// current process is killed. 224 | /// 225 | /// # Arguments 226 | /// 227 | /// `name` - Name for your Mac application. This is what is displayed 228 | /// in the dock and the menu bar. 229 | /// 230 | /// `exe` - Name for the executable file in your application. This is the 231 | /// name of the process that executes, and what appears in `ps` or Activity 232 | /// Monitor. 233 | /// 234 | /// `ident` - Unique application identifier for your app. This should be 235 | /// in the reverse DNS format (ex: `com.company.AppName`), and must contain 236 | /// only alpha-numerics, `-`, and `.` characters. It can be used to 237 | /// register your app as a system-wide handler for documents and URIs, and 238 | /// is used when code signing your app for distribution. 239 | /// 240 | /// # Returns 241 | /// 242 | /// A newly constructed Trampoline builder. 243 | pub fn new(name: &str, exe: &str, ident: &str) -> Trampoline { 244 | Trampoline { 245 | name: name.to_string(), 246 | exe: exe.to_string(), 247 | ident: ident.to_string(), 248 | version: "1.0.0".to_string(), 249 | hidpi: true, 250 | .. 251 | Default::default() 252 | } 253 | } 254 | /// Set name of application. Same as provided to `new()`. 255 | pub fn name(&mut self, name: &str) -> &mut Self { 256 | self.name = name.to_string(); 257 | self 258 | } 259 | /// Set name of executable. Same as provided to `new()`. 260 | pub fn exe(&mut self, exe: &str) -> &mut Self { 261 | self.exe = exe.to_string(); 262 | self 263 | } 264 | /// Set app bundle ID. Same as provided to `new()`. 265 | pub fn ident(&mut self, ident: &str) -> &mut Self { 266 | self.ident = ident.to_string(); 267 | self 268 | } 269 | /// Set bundle icon file. 270 | /// 271 | /// This is the name of the icon file in the Resources directory. It should 272 | /// be just the file name, without a path. OS X uses this icon file for the 273 | /// icon displayed in the Dock when your application is running, and the 274 | /// icon that appears next to it in Finder and the Application list. 275 | /// 276 | /// Icons are typically provided in a multi-icon set file in the `.icns` 277 | /// format. 278 | /// 279 | /// It is optional, but strongly recommended for apps that will be 280 | /// distributed to end users. 281 | pub fn icon(&mut self, icon: &str) -> &mut Self { 282 | self.icon = icon.to_string(); 283 | self 284 | } 285 | /// Set the bundle version. 286 | /// 287 | /// This sets the version number in the app bundle. It is optional, and 288 | /// defaults to "1.0.0" if not provided. 289 | pub fn version(&mut self, version: &str) -> &mut Self { 290 | self.version = version.to_string(); 291 | self 292 | } 293 | /// Set an arbitrary key/value pair in the Info.plist 294 | /// 295 | /// Bundles support specifying a large variety of configuration options in 296 | /// their Property List files, many of which are only needed for specific 297 | /// use cases. This function lets you specify any arbitrary key/value 298 | /// pair that your application might need. 299 | /// 300 | /// Note that some keys are provided with a default value if not specified, 301 | /// and a few keys are always configured by the `Trampoline` builder and 302 | /// cannot be overridden with this function. 303 | /// 304 | /// `Trampoline` creates Info.plist files in the "old-style" OpenStep format. 305 | /// Be sure to format your values appropriately for this style. Read up on 306 | /// [Old-Style ASCII Property Lists](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html). You can also verify your 307 | /// formatting by creating a simple `test.plist` with your key/value pairs 308 | /// in it, surround the entire file in braces (`{` and `}`), and then run 309 | /// `plutil test.plist` to validate the formatting. 310 | /// 311 | /// See the [Apple documentation](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009247) 312 | /// on Info.plist keys for options. 313 | /// 314 | /// # Arguments 315 | /// 316 | /// `key` - Property List key to set (ex: `CFBundleURLTypes`) 317 | /// 318 | /// `value` - Value for the key, in JSON format. You must provide quote 319 | /// characters yourself for any values that require quoted strings. Format 320 | /// in "old-style" OpenStep plist format. 321 | pub fn plist_key(&mut self, key: &str, value: &str) -> &mut Self { 322 | self.keys.push((key.to_string(), value.to_string())); 323 | self 324 | } 325 | /// Set multiple arbitrary key/value pairs in the Info.plist 326 | /// 327 | /// See documentation of [plist_key()](Trampoline::plist_key). This function does the same, but 328 | /// allows specifying more than one key/value pair at a time. 329 | pub fn plist_keys(&mut self, pairs: &Vec<(&str,&str)>) -> &mut Self { 330 | for &(ref key, ref value) in pairs { 331 | self.keys.push((key.to_string(), value.to_string())); 332 | } 333 | self 334 | } 335 | /// Sets whether fruitbasket should add properties to the generated plist 336 | /// to tell macOS that this application supports high resolution displays. 337 | /// 338 | /// This option is true by default. A bit of backstory follows. 339 | /// 340 | /// --- 341 | /// 342 | /// macOS, by default, runs apps in a low-resolution mode if they are 343 | /// in a bundle that does not specify that it supports Retina displays. This 344 | /// causes them to look blurry on Retina displays, not crisp like the rest 345 | /// of macOS. 346 | /// 347 | /// You may not have noticed this behavior when running GUI applications as 348 | /// bare binaries, where it does not apply (macOS does not run binaries in 349 | /// low-resolution mode). However, the Trampoline is different because it 350 | /// automatically bundles the binary, which opens up the application to this 351 | /// kind of legacy behavior. 352 | /// 353 | /// Historically, it was done for backwards compatibility, because when the 354 | /// Retina screen came out in 2012, not all applications supported it. 355 | /// Indeed, some programs even today don't support it either, which is why 356 | /// this behavior remains. However, most GUI facilities like Qt, GTK and 357 | /// libui support Retina just fine, and don't need macOS to sandbox them 358 | /// into this low resolution mode. (libui doesn't even need to do anything 359 | /// special; they use AppKit directly, which has always supported Retina.) 360 | /// 361 | /// Applications that wish to indicate that they *do* support Retina 362 | /// displays have to specify two properties in their Info.plist: 363 | /// - Set `NSPrincipalClass` to something. (`"NSApplication"` is a useful 364 | /// default, but it's unknown what the significance of this property is. 365 | /// fruitbasket uses `"NSApplication"`.) 366 | /// - Set `NSHighResolutionCapable` to `True`. 367 | /// 368 | /// After this is done, macOS will run the app at full resolution and trust 369 | /// it to scale its UI to match the scale factor of the resolution being 370 | /// used. On most displays, it is 2 by default, but macOS supports both 371 | /// larger and also non-integer scale factors. 372 | /// 373 | /// Sometimes you have to do this manually, such as when you are rendering 374 | /// into a framebuffer, and sometimes you don't, like when you are using a 375 | /// GUI toolkit. Usually, programmers can expect their libraries to support 376 | /// this natively and that is why this option is enabled by default. 377 | /// 378 | /// Older versions of fruitbasket did not apply these changes by default, 379 | /// which meant in the best case the developer had to look online for a 380 | /// solution, and in the worst case apps built using fruitbasket ran in 381 | /// low resolution (ouch!). It is my hope that this new default will help 382 | /// both new and experienced developers alike, even though it is a somewhat 383 | /// simple change. 384 | pub fn retina(&mut self, doit: bool) -> &mut Self { 385 | self.hidpi = doit; 386 | self 387 | } 388 | /// Add a 'raw', preformatted string to Info.plist 389 | /// 390 | /// Pastes a raw, unedited string into the Info.plist file. This is 391 | /// dangerous, and should be used with care. Use this for adding nested 392 | /// structures, such as when registering URI schemes. 393 | /// 394 | /// *MUST* be in the JSON plist format. If coming from XML format, you can 395 | /// use `plutil -convert json -r Info.plist` to convert. 396 | /// 397 | /// Take care not to override any of the keys in [FORBIDDEN_PLIST](FORBIDDEN_PLIST) 398 | /// unless you really know what you are doing. 399 | pub fn plist_raw_string(&mut self, s: String) -> &mut Self { 400 | self.plist_raw_strings.push(s); 401 | self 402 | } 403 | /// Add file to Resources directory of app bundle 404 | /// 405 | /// Specify full path to a file to copy into the Resources directory of the 406 | /// generated app bundle. Resources can be any sort of file, and are copied 407 | /// around with the app when it is moved. The app can easily access any 408 | /// file in its resources at runtime, even when running in sandboxed 409 | /// environments. 410 | /// 411 | /// The most common bundled resources are icons. 412 | /// 413 | /// # Arguments 414 | /// 415 | /// `file` - Full path to file to include 416 | pub fn resource(&mut self, file: &str) -> &mut Self { 417 | self.resources.push(file.to_string()); 418 | self 419 | } 420 | 421 | /// Add multiple files to Resources directory of app bundle 422 | /// 423 | /// See documentation of [resource()](Trampoline::resource). This function does the same, but 424 | /// allows specifying more than one resource at a time. 425 | pub fn resources(&mut self, files: &Vec<&str>) -> &mut Self{ 426 | for file in files { 427 | self.resources.push(file.to_string()); 428 | } 429 | self 430 | } 431 | 432 | /// Finishes building and launching the app bundle 433 | /// 434 | /// This builds and executes the "trampoline", meaning it is a highly 435 | /// destructive action. A Mac app bundle will be created on disk if the 436 | /// program is not already executing from one, the new bundle will be 437 | /// launched as a new process, and the currently executing process will 438 | /// be terminated. 439 | /// 440 | /// The behavior, when used as intended, is similar to `fork()` (except 441 | /// the child starts over from `main()` instead of continuing from the 442 | /// same instruction, and the parent dies). The parent dies immediately, 443 | /// the child relaunches, re-runs the `Trampoline` builder, but this time 444 | /// it returns an initialized `FruitApp`. 445 | /// 446 | /// **WARNING**: the parent process is terminated with `exit(0)`, which 447 | /// does not Drop your Rust allocations. This should always be called as 448 | /// early as possible in your program, before any allocations or locking. 449 | /// 450 | /// # Arguments 451 | /// 452 | /// `dir` - Directory to create app bundle in (if one is created) 453 | /// 454 | /// # Returns 455 | /// 456 | /// * Result<_, FruitError> if not running in a bundle and a new bundle 457 | /// could not be created. 458 | /// * Result<_, FruitError> if running in a bundle but the Mac app 459 | /// environment could not be initialized. 460 | /// * Terminates the process if not running in a Mac app bundle and a new 461 | /// bundle was successfully created. 462 | /// * Result if running in a Mac bundle (either when launched 463 | /// from one initially, or successfully re-launched by `Trampoline`) 464 | /// containing the initialized app environment, 465 | pub fn build<'a>(&mut self, dir: InstallDir) -> Result, FruitError> { 466 | self.self_bundle(dir)?; // terminates this process if not bundled 467 | info!("Process is bundled. Continuing."); 468 | Ok(FruitApp::new()) 469 | } 470 | /// Returns whether the current process is running from a Mac app bundle 471 | pub fn is_bundled() -> bool { 472 | unsafe { 473 | let cls = Class::get("NSBundle").unwrap(); 474 | let bundle: *mut Object = msg_send![cls, mainBundle]; 475 | let ident: *mut Object = msg_send![bundle, bundleIdentifier]; 476 | ident != nil 477 | } 478 | } 479 | /// Same as `build`, but does not construct a FruitApp if successful. 480 | /// 481 | /// Useful if you'd like to use a GUI library, such as libui, and don't 482 | /// want fruitbasket to try to initialize anything for you. Bundling only. 483 | pub fn self_bundle(&self, dir: InstallDir) -> Result<(), FruitError> { 484 | unsafe { 485 | if Self::is_bundled() { 486 | return Ok(()); 487 | } 488 | info!("Process not bundled. Self-bundling and relaunching."); 489 | 490 | let install_dir: PathBuf = match dir { 491 | InstallDir::Temp => std::env::temp_dir(), 492 | InstallDir::SystemApplications => PathBuf::from("/Applications/"), 493 | InstallDir::UserApplications => dirs::home_dir().unwrap().join("Applications/"), 494 | InstallDir::Custom(dir) => std::fs::canonicalize(PathBuf::from(dir))?, 495 | }; 496 | info!("Install dir: {:?}", install_dir); 497 | let bundle_dir = Path::new(&install_dir).join(&format!("{}.app", self.name)); 498 | info!("Bundle dir: {:?}", bundle_dir); 499 | let contents_dir = Path::new(&bundle_dir).join("Contents"); 500 | let macos_dir = contents_dir.clone().join("MacOS"); 501 | let resources_dir = contents_dir.clone().join("Resources"); 502 | let plist = contents_dir.clone().join("Info.plist"); 503 | let src_exe = std::env::current_exe()?; 504 | info!("Current exe: {:?}", src_exe); 505 | let dst_exe = macos_dir.clone().join(&self.exe); 506 | 507 | let _ = std::fs::remove_dir_all(&bundle_dir); // ignore errors 508 | std::fs::create_dir_all(&macos_dir)?; 509 | std::fs::create_dir_all(&resources_dir)?; 510 | info!("Copy {:?} to {:?}", src_exe, dst_exe); 511 | std::fs::copy(src_exe, dst_exe)?; 512 | 513 | for file in &self.resources { 514 | let file = Path::new(file); 515 | if let Some(filename) = file.file_name() { 516 | let dst = resources_dir.clone().join(filename); 517 | info!("Copy {:?} to {:?}", file, dst); 518 | std::fs::copy(file, dst)?; 519 | } 520 | } 521 | 522 | // Write Info.plist 523 | let mut f = std::fs::File::create(&plist)?; 524 | 525 | // Mandatory fields 526 | write!(&mut f, "{{\n")?; 527 | write!(&mut f, " CFBundleName = \"{}\";\n", self.name)?; 528 | write!(&mut f, " CFBundleDisplayName = \"{}\";\n", self.name)?; 529 | write!(&mut f, " CFBundleIdentifier = \"{}\";\n", self.ident)?; 530 | write!(&mut f, " CFBundleExecutable = \"{}\";\n", self.exe)?; 531 | write!(&mut f, " CFBundleIconFile = \"{}\";\n", self.icon)?; 532 | write!(&mut f, " CFBundleVersion = \"{}\";\n", self.version)?; 533 | 534 | // HiDPI fields 535 | if self.hidpi { 536 | write!(&mut f, " NSPrincipalClass = \"NSApplication\";\n")?; 537 | write!(&mut f, " NSHighResolutionCapable = True;\n")?; 538 | } 539 | 540 | // User-supplied fields 541 | for &(ref key, ref val) in &self.keys { 542 | if !FORBIDDEN_PLIST.contains(&key.as_str()) { 543 | write!(&mut f, " {} = {};\n", key, val)?; 544 | } 545 | } 546 | 547 | // Default fields (if user didn't override) 548 | let keys: Vec<&str> = self.keys.iter().map(|x| {x.0.as_ref()}).collect(); 549 | for &(ref key, ref val) in DEFAULT_PLIST { 550 | if !keys.contains(key) { 551 | write!(&mut f, " {} = {};\n", key, val)?; 552 | } 553 | } 554 | 555 | // Write raw plist fields 556 | for raw in &self.plist_raw_strings { 557 | write!(&mut f, "{}\n", raw)?; 558 | } 559 | 560 | write!(&mut f, "}}\n")?; 561 | 562 | // Launch newly created bundle 563 | let cls = Class::get("NSWorkspace").unwrap(); 564 | let wspace: *mut Object = msg_send![cls, sharedWorkspace]; 565 | let cls = Class::get("NSString").unwrap(); 566 | let app = bundle_dir.to_str().unwrap(); 567 | info!("Launching: {}", app); 568 | let s: *mut Object = msg_send![cls, alloc]; 569 | let s: *mut Object = msg_send![s, 570 | initWithBytes:app.as_ptr() 571 | length:app.len() 572 | encoding: 4]; // UTF8_ENCODING 573 | let _:() = msg_send![wspace, launchApplication: s]; 574 | 575 | // Note: launchedApplication doesn't return until the child process 576 | // calls [NSApplication sharedApplication]. 577 | info!("Parent process exited."); 578 | std::process::exit(0); 579 | } 580 | } 581 | } 582 | 583 | impl<'a> FruitApp<'a> { 584 | /// Initialize the Apple app environment 585 | /// 586 | /// Initializes the NSApplication singleton that initializes the Mac app 587 | /// environment and creates a memory pool for Objective-C allocations on 588 | /// the main thread. 589 | /// 590 | /// # Returns 591 | /// 592 | /// A newly allocated FruitApp for managing the app 593 | pub fn new() -> FruitApp<'a> { 594 | let (tx,rx) = channel::<()>(); 595 | unsafe { 596 | let cls = Class::get("NSApplication").unwrap(); 597 | let app: *mut Object = msg_send![cls, sharedApplication]; 598 | let cls = Class::get("NSAutoreleasePool").unwrap(); 599 | let pool: *mut Object = msg_send![cls, alloc]; 600 | let pool: *mut Object = msg_send![pool, init]; 601 | let cls = Class::get("NSString").unwrap(); 602 | let rust_runmode = "kCFRunLoopDefaultMode"; 603 | let run_mode: *mut Object = msg_send![cls, alloc]; 604 | let run_mode: *mut Object = msg_send![run_mode, 605 | initWithBytes:rust_runmode.as_ptr() 606 | length:rust_runmode.len() 607 | encoding: 4]; // UTF8_ENCODING 608 | let objc = ObjcSubclass::new().share(); 609 | let rustobjc = Box::new(ObjcWrapper { 610 | objc: objc, 611 | map: HashMap::new(), 612 | }); 613 | let ptr: u64 = &*rustobjc as *const ObjcWrapper as u64; 614 | let _:() = msg_send![rustobjc.objc, setRustWrapper: ptr]; 615 | FruitApp { 616 | app: app, 617 | pool: Cell::new(pool), 618 | run_count: Cell::new(0), 619 | run_mode: run_mode, 620 | tx: tx, 621 | rx: rx, 622 | objc: rustobjc, 623 | } 624 | } 625 | } 626 | 627 | /// Register to receive a callback when the ObjC runtime raises one 628 | /// 629 | /// ObjCCallbackKey is used to specify the source of the callback, which 630 | /// must be something registered with the ObjC runtime. 631 | /// 632 | pub fn register_callback(&mut self, key: FruitCallbackKey, cb: FruitObjcCallback<'a>) { 633 | let _ = self.objc.map.insert(key, cb); 634 | } 635 | 636 | /// Register application to receive Apple events of the given type 637 | /// 638 | /// Register with the underlying NSAppleEventManager so this application gets 639 | /// events matching the given Class/ID tuple. This causes the internal ObjC 640 | /// delegate to receive `handleEvent:withReplyEvent:` messages when the 641 | /// specified event is sent to your application. 642 | /// 643 | /// This registers the event to be received internally. To receive it in 644 | /// your code, you must use [register_callback](FruitApp::register_callback) to listen for the 645 | /// selector by specifying key: 646 | /// 647 | /// `FruitCallbackKey::Method("handleEvent:withReplyEvent:")` 648 | /// 649 | pub fn register_apple_event(&mut self, class: u32, id: u32) { 650 | unsafe { 651 | let cls = Class::get("NSAppleEventManager").unwrap(); 652 | let manager: *mut Object = msg_send![cls, sharedAppleEventManager]; 653 | let objc = (*self.objc).take(); 654 | let _:() = msg_send![manager, 655 | setEventHandler: objc 656 | andSelector: sel!(handleEvent:withReplyEvent:) 657 | forEventClass: class 658 | andEventID: id]; 659 | } 660 | } 661 | 662 | /// Set the app "activation policy" controlling what UI it does/can present. 663 | pub fn set_activation_policy(&self, policy: ActivationPolicy) { 664 | let policy_int = match policy { 665 | ActivationPolicy::Regular => 0, 666 | ActivationPolicy::Accessory => 1, 667 | ActivationPolicy::Prohibited => 2, 668 | }; 669 | unsafe { 670 | let _:() = msg_send![self.app, setActivationPolicy: policy_int]; 671 | } 672 | } 673 | 674 | /// Cleanly terminate the application 675 | /// 676 | /// Terminates a running application and its event loop, and terminates the 677 | /// process. This function does not return, so perform any required cleanup 678 | /// of your Rust application before calling it. 679 | /// 680 | /// You should call this at the end of your program instead of simply exiting 681 | /// from `main()` to ensure that OS X knows your application has quit cleanly 682 | /// and can immediately inform any subsystems that are monitoring it. 683 | /// 684 | /// This can be called from any thread. 685 | /// 686 | /// # Arguments 687 | /// 688 | /// `exit_code` - Application exit code. '0' is success. 689 | pub fn terminate(exit_code: i32) { 690 | unsafe { 691 | let cls = objc::runtime::Class::get("NSApplication").unwrap(); 692 | let app: *mut objc::runtime::Object = msg_send![cls, sharedApplication]; 693 | let _:() = msg_send![app, terminate: exit_code]; 694 | } 695 | } 696 | 697 | /// Stop the running app run loop 698 | /// 699 | /// If the run loop is running (`run()`), this stops it after the next event 700 | /// finishes processing. It does not quit or terminate anything, and the 701 | /// run loop can be continued later. This can be used from callbacks to 702 | /// interrupt a run loop running in 'Forever' mode and return control back 703 | /// to Rust's main thread. 704 | /// 705 | /// This can be called from any thread. 706 | /// 707 | /// # Arguments 708 | /// 709 | /// `stopper` - A thread-safe `FruitStopper` object returned by `stopper()` 710 | pub fn stop(stopper: &FruitStopper) { 711 | stopper.stop(); 712 | } 713 | 714 | /// Runs the main application event loop 715 | /// 716 | /// The application's event loop must be run frequently to dispatch all 717 | /// events generated by the Apple frameworks to their destinations and keep 718 | /// the UI updated. Take care to keep this running frequently, as any 719 | /// delays will cause the UI to hang and cause latency on other internal 720 | /// operations. 721 | /// 722 | /// # Arguments 723 | /// 724 | /// `period` - How long to run the event loop before returning 725 | /// 726 | /// # Returns 727 | /// 728 | /// Ok on natural end, Err if stopped by a Stopper. 729 | pub fn run(&mut self, period: RunPeriod) -> Result<(),()>{ 730 | let start = time::now_utc().to_timespec(); 731 | loop { 732 | if self.rx.try_recv().is_ok() { 733 | return Err(()); 734 | } 735 | unsafe { 736 | let run_count = self.run_count.get(); 737 | if run_count == 0 { 738 | let cls = objc::runtime::Class::get("NSApplication").unwrap(); 739 | let app: *mut objc::runtime::Object = msg_send![cls, sharedApplication]; 740 | let objc = (*self.objc).take(); 741 | let _:() = msg_send![app, setDelegate: objc]; 742 | let _:() = msg_send![self.app, finishLaunching]; 743 | } 744 | // Create a new release pool every once in a while, draining the old one 745 | if run_count % 100 == 0 { 746 | let old_pool = self.pool.get(); 747 | if run_count != 0 { 748 | let _:() = msg_send![old_pool, drain]; 749 | } 750 | let cls = Class::get("NSAutoreleasePool").unwrap(); 751 | let pool: *mut Object = msg_send![cls, alloc]; 752 | let pool: *mut Object = msg_send![pool, init]; 753 | self.pool.set(pool); 754 | } 755 | let mode = self.run_mode; 756 | let event: *mut Object = msg_send![self.app, 757 | nextEventMatchingMask: 0xffffffffffffffffu64 758 | untilDate: nil 759 | inMode: mode 760 | dequeue: 1]; 761 | let _:() = msg_send![self.app, sendEvent: event]; 762 | let _:() = msg_send![self.app, updateWindows]; 763 | self.run_count.set(run_count + 1); 764 | } 765 | if period == RunPeriod::Once { 766 | break; 767 | } 768 | thread::sleep(Duration::from_millis(50)); 769 | if let RunPeriod::Time(t) = period { 770 | let now = time::now_utc().to_timespec(); 771 | if now >= start + time::Duration::from_std(t).unwrap() { 772 | break; 773 | } 774 | } 775 | } 776 | return Ok(()); 777 | } 778 | /// Create a thread-safe object that can interrupt the run loop 779 | /// 780 | /// Returns an object that is safe to pass across thread boundaries (i.e. 781 | /// it implements Send and Sync), and can be used to interrupt and stop 782 | /// the run loop, even when running in 'Forever' mode. 783 | /// 784 | /// This makes it convenient to implement the common strategy of blocking 785 | /// the main loop forever on the Apple run loop, until some other UI or 786 | /// processing thread interrupts it and lets the main thread handle cleanup 787 | /// and graceful shutdown. 788 | /// 789 | /// # Returns 790 | /// 791 | /// A newly allocated object that can be passed across thread boundaries and 792 | /// cloned infinite times.. 793 | pub fn stopper(&self) -> FruitStopper { 794 | FruitStopper { 795 | tx: self.tx.clone() 796 | } 797 | } 798 | 799 | /// Locate a resource in the executing Mac App bundle 800 | /// 801 | /// Looks for a resource by name and extension in the bundled Resources 802 | /// directory. 803 | /// 804 | /// # Arguments 805 | /// 806 | /// `name` - Name of the file to find, without the extension 807 | /// 808 | /// `extension` - Extension of the file to find. Can be an empty string for 809 | /// files with no extension. 810 | /// 811 | /// # Returns 812 | /// 813 | /// The full, absolute path to the resource, or None if not found. 814 | pub fn bundled_resource_path(name: &str, extension: &str) -> Option { 815 | unsafe { 816 | let cls = Class::get("NSBundle").unwrap(); 817 | let bundle: *mut Object = msg_send![cls, mainBundle]; 818 | let cls = Class::get("NSString").unwrap(); 819 | let objc_str: *mut Object = msg_send![cls, alloc]; 820 | let objc_name: *mut Object = msg_send![objc_str, 821 | initWithBytes:name.as_ptr() 822 | length:name.len() 823 | encoding: 4]; // UTF8_ENCODING 824 | let objc_str: *mut Object = msg_send![cls, alloc]; 825 | let objc_ext: *mut Object = msg_send![objc_str, 826 | initWithBytes:extension.as_ptr() 827 | length:extension.len() 828 | encoding: 4]; // UTF8_ENCODING 829 | let ini: *mut Object = msg_send![bundle, 830 | pathForResource:objc_name 831 | ofType:objc_ext]; 832 | let _:() = msg_send![objc_name, release]; 833 | let _:() = msg_send![objc_ext, release]; 834 | let cstr: *const i8 = msg_send![ini, UTF8String]; 835 | if cstr != std::ptr::null() { 836 | let rstr = std::ffi::CStr::from_ptr(cstr).to_string_lossy().into_owned(); 837 | return Some(rstr); 838 | } 839 | None 840 | } 841 | } 842 | } 843 | 844 | /// Parse an Apple URL event into a URL string 845 | /// 846 | /// Takes an NSAppleEventDescriptor from an Apple URL event, unwraps 847 | /// it, and returns the contained URL as a String. 848 | pub fn parse_url_event(event: *mut Object) -> String { 849 | if event as u64 == 0u64 { 850 | return "".into(); 851 | } 852 | unsafe { 853 | let class: u32 = msg_send![event, eventClass]; 854 | let id: u32 = msg_send![event, eventID]; 855 | if class != ::kInternetEventClass || id != ::kAEGetURL { 856 | return "".into(); 857 | } 858 | let subevent: *mut Object = msg_send![event, paramDescriptorForKeyword: ::keyDirectObject]; 859 | let nsstring: *mut Object = msg_send![subevent, stringValue]; 860 | nsstring_to_string(nsstring) 861 | } 862 | } 863 | 864 | /// Convert an NSString to a Rust `String` 865 | pub fn nsstring_to_string(nsstring: *mut Object) -> String { 866 | unsafe { 867 | let cstr: *const i8 = msg_send![nsstring, UTF8String]; 868 | if cstr != std::ptr::null() { 869 | std::ffi::CStr::from_ptr(cstr) 870 | .to_string_lossy() 871 | .into_owned() 872 | } else { 873 | "".into() 874 | } 875 | } 876 | } 877 | 878 | /// ObjcSubclass is a subclass of the objective-c NSObject base class. 879 | /// This is registered with the objc runtime, so instances of this class 880 | /// are "owned" by objc, and have no associated Rust data. 881 | /// 882 | /// This can be wrapped with a ObjcWrapper, which is a proper Rust struct 883 | /// with its own storage, and holds an instance of ObjcSubclass. 884 | /// 885 | /// An ObjcSubclass can "talk" to its Rust wrapper class through function 886 | /// pointers, as long as the storage is on the heap with a Box and the underlying 887 | /// memory address doesn't change. 888 | /// 889 | enum ObjcSubclass {} 890 | 891 | unsafe impl Message for ObjcSubclass { } 892 | 893 | static OBJC_SUBCLASS_REGISTER_CLASS: Once = ONCE_INIT; 894 | 895 | impl ObjcSubclass { 896 | /// Call a registered Rust callback 897 | fn dispatch_cb(wrap_ptr: u64, key: FruitCallbackKey, obj: *mut Object) { 898 | if wrap_ptr == 0 { 899 | return; 900 | } 901 | let objcwrap: &mut ObjcWrapper = unsafe { &mut *(wrap_ptr as *mut ObjcWrapper) }; 902 | if let Some(ref cb) = objcwrap.map.get(&key) { 903 | cb(obj); 904 | } 905 | } 906 | } 907 | 908 | /// Define an ObjC class and register it with the ObjC runtime 909 | impl INSObject for ObjcSubclass { 910 | fn class() -> &'static Class { 911 | OBJC_SUBCLASS_REGISTER_CLASS.call_once(|| { 912 | let superclass = NSObject::class(); 913 | let mut decl = ClassDecl::new("ObjcSubclass", superclass).unwrap(); 914 | decl.add_ivar::("_rustwrapper"); 915 | 916 | /// Callback for events from Apple's NSAppleEventManager 917 | extern fn objc_apple_event(this: &Object, _cmd: Sel, event: u64, _reply: u64) { 918 | let ptr: u64 = unsafe { *this.get_ivar("_rustwrapper") }; 919 | ObjcSubclass::dispatch_cb(ptr, 920 | FruitCallbackKey::Method("handleEvent:withReplyEvent:"), 921 | event as *mut Object); 922 | } 923 | /// NSApplication delegate callback 924 | extern fn objc_did_finish(this: &Object, _cmd: Sel, event: u64) { 925 | let ptr: u64 = unsafe { *this.get_ivar("_rustwrapper") }; 926 | ObjcSubclass::dispatch_cb(ptr, 927 | FruitCallbackKey::Method("applicationDidFinishLaunching:"), 928 | event as *mut Object); 929 | } 930 | /// NSApplication delegate callback 931 | extern fn objc_will_finish(this: &Object, _cmd: Sel, event: u64) { 932 | let ptr: u64 = unsafe { *this.get_ivar("_rustwrapper") }; 933 | ObjcSubclass::dispatch_cb(ptr, 934 | FruitCallbackKey::Method("applicationWillFinishLaunching:"), 935 | event as *mut Object); 936 | } 937 | /// NSApplication delegate callback 938 | extern "C" fn objc_open_file( 939 | this: &Object, 940 | _cmd: Sel, 941 | _application: u64, 942 | file: u64, 943 | ) -> bool { 944 | let ptr: u64 = unsafe { *this.get_ivar("_rustwrapper") }; 945 | ObjcSubclass::dispatch_cb( 946 | ptr, 947 | FruitCallbackKey::Method("application:openFile:"), 948 | file as *mut Object, 949 | ); 950 | 951 | true 952 | } 953 | /// Register the Rust ObjcWrapper instance that wraps this object 954 | /// 955 | /// In order for an instance of this ObjC owned object to reach back 956 | /// into "pure Rust", it needs to know the location of Rust 957 | /// functions. This is accomplished by wrapping it in a Rust struct, 958 | /// which is itself in a Box on the heap to ensure a fixed location 959 | /// in memory. The address of this wrapping struct is given to this 960 | /// object by casting the Box into a raw pointer, and then casting 961 | /// that into a u64, which is stored here. 962 | extern fn objc_set_rust_wrapper(this: &mut Object, _cmd: Sel, ptr: u64) { 963 | unsafe {this.set_ivar("_rustwrapper", ptr);} 964 | } 965 | unsafe { 966 | // Register all of the above handlers as true ObjC selectors: 967 | let f: extern fn(&mut Object, Sel, u64) = objc_set_rust_wrapper; 968 | decl.add_method(sel!(setRustWrapper:), f); 969 | let f: extern fn(&Object, Sel, u64, u64) = objc_apple_event; 970 | decl.add_method(sel!(handleEvent:withReplyEvent:), f); 971 | let f: extern fn(&Object, Sel, u64) = objc_did_finish; 972 | decl.add_method(sel!(applicationDidFinishLaunching:), f); 973 | let f: extern fn(&Object, Sel, u64) = objc_will_finish; 974 | decl.add_method(sel!(applicationWillFinishLaunching:), f); 975 | let f: extern "C" fn(&Object, Sel, u64, u64) -> bool = objc_open_file; 976 | decl.add_method(sel!(application:openFile:), f); 977 | } 978 | 979 | decl.register(); 980 | }); 981 | 982 | Class::get("ObjcSubclass").unwrap() 983 | } 984 | } 985 | --------------------------------------------------------------------------------