Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions examples/terminal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
extern crate fruitbasket;

use std::io::Write;
use fruitbasket::{ActivationPolicy, FruitApp, FruitCallbackKey, FruitError, Trampoline};

#[macro_use]
extern crate log;

fn main() {
// Setup logging. Requires 'logging' feature to be enabled.
let _ = fruitbasket::create_logger(".fruitbasket_terminal.log", fruitbasket::LogDir::Home, 5, 2).unwrap();

let mut app = match Trampoline::new("fruitbasket_terminal", "fruitbasket_terminal", "com.trevorbentley.fruitbasket_terminal")
.version(env!("CARGO_PKG_VERSION"))
.plist_key("CFBundleSpokenName", "fruit basket terminal")
.build(fruitbasket::InstallDir::Temp)
{
Err(FruitError::UnsupportedPlatform(_)) => {
println!("This is not a Mac. App bundling is not supported.");
// It is still safe to use FruitApp::new(),
// though the dummy app will do nothing.
FruitApp::new()
},
Err(FruitError::IOError(e)) => {
println!("IO error! {}", e);
std::process::exit(1);
},
Err(FruitError::GeneralError(e)) => {
println!("General error! {}", e);
std::process::exit(1);
},
Ok(app) => app,
};

// Make it a regular app in the dock.
// Note: Because 'LSBackgroundOnly' is set to true in the Info.plist, the
// app will launch backgrounded and will not take focus. If we only did
// that, the app would stay in 'Prohibited' mode and would not create a dock
// icon. By overriding the activation policy now, it will stay background
// but create the Dock and menu bar entries. This basically implements a
// "pop-under" behavior.
app.set_activation_policy(ActivationPolicy::Regular);

// Register a callback for when the ObjC application finishes launching
let stopper = app.stopper();
app.register_callback(FruitCallbackKey::Method("applicationWillFinishLaunching:"),
Box::new(move |_event| {
info!("applicationDidFinishLaunching.");
stopper.stop();
}));

// Run until callback is called
info!("Spawned process started. Run until applicationDidFinishLaunching.");
let _ = app.run(fruitbasket::RunPeriod::Forever);

// Print a prompt and read a line of input:
// > This should come before the prompt.
// > Please enter your name: Harry Potter
// > Hello, Harry Potter!
print!("Please enter your name: ");
eprintln!("This should come before the prompt.");
std::io::stdout().flush().unwrap();
let mut name = String::new();
std::io::stdin().read_line(&mut name)
.expect("Failed to read line");
if name.ends_with('\n') {
name.pop();
}
println!("Hello, {}!", name);

// Cleanly terminate
fruitbasket::FruitApp::terminate(0);
println!("This will not print.");
}
160 changes: 73 additions & 87 deletions src/osx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,102 +481,88 @@ impl Trampoline {
/// Useful if you'd like to use a GUI library, such as libui, and don't
/// want fruitbasket to try to initialize anything for you. Bundling only.
pub fn self_bundle(&self, dir: InstallDir) -> Result<(), FruitError> {
unsafe {
if Self::is_bundled() {
return Ok(());
}
info!("Process not bundled. Self-bundling and relaunching.");

let install_dir: PathBuf = match dir {
InstallDir::Temp => std::env::temp_dir(),
InstallDir::SystemApplications => PathBuf::from("/Applications/"),
InstallDir::UserApplications => dirs::home_dir().unwrap().join("Applications/"),
InstallDir::Custom(dir) => std::fs::canonicalize(PathBuf::from(dir))?,
};
info!("Install dir: {:?}", install_dir);
let bundle_dir = Path::new(&install_dir).join(&format!("{}.app", self.name));
info!("Bundle dir: {:?}", bundle_dir);
let contents_dir = Path::new(&bundle_dir).join("Contents");
let macos_dir = contents_dir.clone().join("MacOS");
let resources_dir = contents_dir.clone().join("Resources");
let plist = contents_dir.clone().join("Info.plist");
let src_exe = std::env::current_exe()?;
info!("Current exe: {:?}", src_exe);
let dst_exe = macos_dir.clone().join(&self.exe);

let _ = std::fs::remove_dir_all(&bundle_dir); // ignore errors
std::fs::create_dir_all(&macos_dir)?;
std::fs::create_dir_all(&resources_dir)?;
info!("Copy {:?} to {:?}", src_exe, dst_exe);
std::fs::copy(src_exe, dst_exe)?;

for file in &self.resources {
let file = Path::new(file);
if let Some(filename) = file.file_name() {
let dst = resources_dir.clone().join(filename);
info!("Copy {:?} to {:?}", file, dst);
std::fs::copy(file, dst)?;
}
}
if Self::is_bundled() {
return Ok(());
}
info!("Process not bundled. Self-bundling and relaunching.");

// Write Info.plist
let mut f = std::fs::File::create(&plist)?;

// Mandatory fields
write!(&mut f, "{{\n")?;
write!(&mut f, " CFBundleName = \"{}\";\n", self.name)?;
write!(&mut f, " CFBundleDisplayName = \"{}\";\n", self.name)?;
write!(&mut f, " CFBundleIdentifier = \"{}\";\n", self.ident)?;
write!(&mut f, " CFBundleExecutable = \"{}\";\n", self.exe)?;
write!(&mut f, " CFBundleIconFile = \"{}\";\n", self.icon)?;
write!(&mut f, " CFBundleVersion = \"{}\";\n", self.version)?;

// HiDPI fields
if self.hidpi {
write!(&mut f, " NSPrincipalClass = \"NSApplication\";\n")?;
write!(&mut f, " NSHighResolutionCapable = True;\n")?;
let install_dir: PathBuf = match dir {
InstallDir::Temp => std::env::temp_dir(),
InstallDir::SystemApplications => PathBuf::from("/Applications/"),
InstallDir::UserApplications => dirs::home_dir().unwrap().join("Applications/"),
InstallDir::Custom(dir) => std::fs::canonicalize(PathBuf::from(dir))?,
};
info!("Install dir: {:?}", install_dir);
let bundle_dir = Path::new(&install_dir).join(&format!("{}.app", self.name));
info!("Bundle dir: {:?}", bundle_dir);
let contents_dir = Path::new(&bundle_dir).join("Contents");
let macos_dir = contents_dir.clone().join("MacOS");
let resources_dir = contents_dir.clone().join("Resources");
let plist = contents_dir.clone().join("Info.plist");
let src_exe = std::env::current_exe()?;
info!("Current exe: {:?}", src_exe);
let dst_exe = macos_dir.clone().join(&self.exe);

let _ = std::fs::remove_dir_all(&bundle_dir); // ignore errors
std::fs::create_dir_all(&macos_dir)?;
std::fs::create_dir_all(&resources_dir)?;
info!("Copy {:?} to {:?}", src_exe, dst_exe);
std::fs::copy(src_exe, &dst_exe)?;

for file in &self.resources {
let file = Path::new(file);
if let Some(filename) = file.file_name() {
let dst = resources_dir.clone().join(filename);
info!("Copy {:?} to {:?}", file, dst);
std::fs::copy(file, dst)?;
}
}

// User-supplied fields
for &(ref key, ref val) in &self.keys {
if !FORBIDDEN_PLIST.contains(&key.as_str()) {
write!(&mut f, " {} = {};\n", key, val)?;
}
}
// Write Info.plist
let mut f = std::fs::File::create(&plist)?;

// Mandatory fields
write!(&mut f, "{{\n")?;
write!(&mut f, " CFBundleName = \"{}\";\n", self.name)?;
write!(&mut f, " CFBundleDisplayName = \"{}\";\n", self.name)?;
write!(&mut f, " CFBundleIdentifier = \"{}\";\n", self.ident)?;
write!(&mut f, " CFBundleExecutable = \"{}\";\n", self.exe)?;
write!(&mut f, " CFBundleIconFile = \"{}\";\n", self.icon)?;
write!(&mut f, " CFBundleVersion = \"{}\";\n", self.version)?;

// HiDPI fields
if self.hidpi {
write!(&mut f, " NSPrincipalClass = \"NSApplication\";\n")?;
write!(&mut f, " NSHighResolutionCapable = True;\n")?;
}

// Default fields (if user didn't override)
let keys: Vec<&str> = self.keys.iter().map(|x| {x.0.as_ref()}).collect();
for &(ref key, ref val) in DEFAULT_PLIST {
if !keys.contains(key) {
write!(&mut f, " {} = {};\n", key, val)?;
}
// User-supplied fields
for &(ref key, ref val) in &self.keys {
if !FORBIDDEN_PLIST.contains(&key.as_str()) {
write!(&mut f, " {} = {};\n", key, val)?;
}
}

// Write raw plist fields
for raw in &self.plist_raw_strings {
write!(&mut f, "{}\n", raw)?;
// Default fields (if user didn't override)
let keys: Vec<&str> = self.keys.iter().map(|x| {x.0.as_ref()}).collect();
for &(ref key, ref val) in DEFAULT_PLIST {
if !keys.contains(key) {
write!(&mut f, " {} = {};\n", key, val)?;
}
}

write!(&mut f, "}}\n")?;

// Launch newly created bundle
let cls = Class::get("NSWorkspace").unwrap();
let wspace: *mut Object = msg_send![cls, sharedWorkspace];
let cls = Class::get("NSString").unwrap();
let app = bundle_dir.to_str().unwrap();
info!("Launching: {}", app);
let s: *mut Object = msg_send![cls, alloc];
let s: *mut Object = msg_send![s,
initWithBytes:app.as_ptr()
length:app.len()
encoding: 4]; // UTF8_ENCODING
let _:() = msg_send![wspace, launchApplication: s];

// Note: launchedApplication doesn't return until the child process
// calls [NSApplication sharedApplication].
info!("Parent process exited.");
std::process::exit(0);
// Write raw plist fields
for raw in &self.plist_raw_strings {
write!(&mut f, "{}\n", raw)?;
}

write!(&mut f, "}}\n")?;

// Launch newly created bundle
info!("Launching: {}", bundle_dir.to_str().unwrap());
std::process::Command::new(dst_exe).spawn()?.wait()?;
info!("Child process exited.");
std::process::exit(0);
}
}

Expand Down