summary history branches tags files
commit:bbb3d0e88759321f1fd7d04cb55afc431a6b0bee
author:Trevor Bentley
committer:Trevor Bentley
date:Sun Jul 30 16:13:26 2017 +0200
parents:eae57f0726a434dc8763e710f269d3c84788ab03
Switch to self-bundling and Mac app handling with fruitbasket
diff --git a/Cargo.toml b/Cargo.toml
line changes: +6/-7
index 656e25f..da27fc3
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,15 +1,14 @@
 [package]
 name = "connectr"
 version = "0.1.1-rc"
-authors = [ "Trevor Bentley <trevor@trevorbentley.com>" ]
-
+authors = [ "Trevor Bentley <mrmekon@gmail.com>" ]
 description = "Spotify Connect library and systray/menubar application for controlling Spotify devices."
 keywords = ["spotify", "connect", "webapi", "systray", "menubar"]
 categories = ["api-bindings"]
 homepage = "https://github.com/mrmekon/connectr"
 repository = "https://github.com/mrmekon/connectr"
+documentation = "https://mrmekon.github.io/connectr/connectr/"
 license = "Apache-2.0"
-
 build = "build.rs"
 
 [lib]
@@ -45,10 +44,10 @@ rust-ini = "0.9"
 time = "0.1"
 timer = "0.1.6"
 chrono = "0.3.0"
-libc = "0.2"
 log = "0.3.7"
 log4rs = "0.6.3"
 ctrlc = "3.0.1"
+fruitbasket = "0.4"
 
 [target."cfg(windows)".dependencies]
 #systray = "0.1.1"
@@ -57,7 +56,7 @@ systray = {path = "deps/systray-rs", version="0.1.1-connectr"}
 
 [target."cfg(windows)".dependencies.rubrail]
 default-features=false
-version = "0.4"
+version = "0.5"
 #path = "deps/rubrail-rs"
 #version="0.4.3-rc"
 
@@ -65,7 +64,7 @@ version = "0.4"
 
 [target."cfg(all(unix, not(target_os = \"macos\")))".dependencies.rubrail]
 default-features = false
-version = "0.4"
+version = "0.5"
 #path = "deps/rubrail-rs"
 #version = "0.4.3-rc"
 
@@ -73,7 +72,7 @@ version = "0.4"
 cocoa = "0.9"
 objc-foundation = "0.1.1"
 objc_id = "0.1"
-rubrail = "0.4"
+rubrail = "0.5"
 #rubrail = {path = "deps/rubrail-rs", version="0.4.3-rc"}
 
 [target."cfg(target_os = \"macos\")".dependencies.objc]

diff --git a/connectr.xcf b/connectr.xcf
line changes: +0/-0
index 27f3ce5..d065bbe
--- a/connectr.xcf
+++ b/connectr.xcf

diff --git a/deps/rubrail-rs b/deps/rubrail-rs
line changes: +1/-1
index b56ccf5..6233cb4
--- a/deps/rubrail-rs
+++ b/deps/rubrail-rs
@@ -1 +1 @@
-Subproject commit b56ccf5ddc4de61dcff892a61ba8de5cc92c5539
+Subproject commit 6233cb4575014f374c2f2549b2438f9e32f9d809

diff --git a/icon/connectr.icns b/icon/connectr.icns
line changes: +0/-0
index 0000000..ca9553a
--- /dev/null
+++ b/icon/connectr.icns

diff --git a/icon/connectr1024.png b/icon/connectr1024.png
line changes: +0/-0
index 0000000..18d782a
--- /dev/null
+++ b/icon/connectr1024.png

diff --git a/icon/gen_iconset.sh b/icon/gen_iconset.sh
line changes: +15/-0
index 0000000..ff6998c
--- /dev/null
+++ b/icon/gen_iconset.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+NAME="connectr"
+mkdir "$NAME.iconset"
+sips -z 16 16     "${NAME}1024.png" --out "$NAME.iconset"/icon_16x16.png
+sips -z 32 32     "${NAME}1024.png" --out "$NAME.iconset"/icon_16x16@2x.png
+sips -z 32 32     "${NAME}1024.png" --out "$NAME.iconset"/icon_32x32.png
+sips -z 64 64     "${NAME}1024.png" --out "$NAME.iconset"/icon_32x32@2x.png
+sips -z 128 128   "${NAME}1024.png" --out "$NAME.iconset"/icon_128x128.png
+sips -z 256 256   "${NAME}1024.png" --out "$NAME.iconset"/icon_128x128@2x.png
+sips -z 256 256   "${NAME}1024.png" --out "$NAME.iconset"/icon_256x256.png
+sips -z 512 512   "${NAME}1024.png" --out "$NAME.iconset"/icon_256x256@2x.png
+sips -z 512 512   "${NAME}1024.png" --out "$NAME.iconset"/icon_512x512.png
+cp "${NAME}1024.png" "$NAME.iconset"/icon_512x512@2x.png
+iconutil -c icns "$NAME.iconset"
+rm -R "$NAME.iconset"

diff --git a/src/main.rs b/src/main.rs
line changes: +23/-1
index 097d0b9..6ee5868
--- a/src/main.rs
+++ b/src/main.rs
@@ -12,6 +12,8 @@ use rubrail::ImageTemplate;
 use rubrail::SpacerType;
 use rubrail::SwipeState;
 
+extern crate fruitbasket;
+
 extern crate ctrlc;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
@@ -785,6 +787,22 @@ fn create_spotify_thread(rx_cmd: Receiver<String>) -> SpotifyThread {
 }
 
 fn main() {
+    // Relaunch in a Mac app bundle if running on OS X and not already bundled.
+    let icon = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+        .join("icon").join("connectr.icns");
+    let touchbar_icon = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+        .join("connectr_80px_300dpi.png");
+    if let Ok(nsapp) = fruitbasket::Trampoline::new(
+        "connectr", "connectr", "com.trevorbentley.connectr")
+        .icon("connectr.icns")
+        .version(env!("CARGO_PKG_VERSION"))
+        .plist_key("LSBackgroundOnly", "1")
+        .resource(icon.to_str().unwrap())
+        .resource(touchbar_icon.to_str().unwrap())
+        .build(fruitbasket::InstallDir::Custom("target/".to_string())) {
+            nsapp.set_activation_policy(fruitbasket::ActivationPolicy::Prohibited);
+        }
+
     create_logger();
     info!("Started Connectr");
 
@@ -832,11 +850,15 @@ fn main() {
         warn!("Didn't find Wine in search path.");
     }
 
+    let mut need_redraw: bool = false;
     while running.load(Ordering::SeqCst) {
         if spotify_thread.rx.recv_timeout(Duration::from_millis(100)).is_ok() {
-            // TODO: && status.can_redraw()
+            need_redraw = true;
+        }
+        if need_redraw && status.can_redraw() {
             clear_menu(&mut app, &mut status);
             fill_menu(&mut app, &spotify_thread, &mut status, &mut touchbar);
+            need_redraw = false;
         }
         status.run(false);
     }

diff --git a/src/osx/mod.rs b/src/osx/mod.rs
line changes: +16/-72
index 41c607e..2a9753b
--- a/src/osx/mod.rs
+++ b/src/osx/mod.rs
@@ -3,7 +3,9 @@ mod rustnsobject;
 extern crate objc;
 extern crate objc_foundation;
 extern crate cocoa;
-extern crate libc;
+
+extern crate fruitbasket;
+use self::fruitbasket::FruitApp;
 
 pub use ::TStatusBar;
 pub use ::NSCallback;
@@ -12,11 +14,8 @@ use objc::runtime::Class;
 
 use self::cocoa::base::{nil, YES};
 use self::cocoa::appkit::NSStatusBar;
-use self::cocoa::foundation::{NSAutoreleasePool,NSString};
-use self::cocoa::appkit::{NSApp,
-                          NSApplication,
-                          NSApplicationActivationPolicyAccessory,
-                          NSMenu,
+use self::cocoa::foundation::NSString;
+use self::cocoa::appkit::{NSMenu,
                           NSMenuItem,
                           NSImage,
                           NSVariableStatusItemLength,
@@ -27,29 +26,15 @@ use self::rustnsobject::{NSObj, NSObjTrait, NSObjCallbackTrait};
 
 use std::sync::mpsc::Sender;
 use std::ptr;
-use std::cell::Cell;
 use std::ffi::CStr;
-use std::thread::sleep;
-use std::time::Duration;
-
-extern crate objc_id;
-use self::objc_id::Id;
 
 pub type Object = objc::runtime::Object;
 
 pub struct OSXStatusBar {
     object: NSObj,
-    app: *mut objc::runtime::Object,
+    app: FruitApp,
     status_bar_item: *mut objc::runtime::Object,
     menu_bar: *mut objc::runtime::Object,
-
-    // Run loop state
-    // Keeping these in persistent state instead of recalculating saves quite a
-    // bit of CPU during idle.
-    pool: Cell<*mut objc::runtime::Object>,
-    run_count: Cell<u64>,
-    run_mode: *mut objc::runtime::Object,
-    run_date: *mut objc::runtime::Object,
 }
 
 impl TStatusBar for OSXStatusBar {
@@ -57,21 +42,15 @@ impl TStatusBar for OSXStatusBar {
     fn new(tx: Sender<String>) -> OSXStatusBar {
         let mut bar;
         unsafe {
-            let app = NSApp();
+            let nsapp = FruitApp::new();
+            nsapp.set_activation_policy(fruitbasket::ActivationPolicy::Prohibited);
             let status_bar = NSStatusBar::systemStatusBar(nil);
-            let date_cls = Class::get("NSDate").unwrap();
             bar = OSXStatusBar {
-                app: app,
+                app: nsapp,
                 status_bar_item: status_bar.statusItemWithLength_(NSVariableStatusItemLength),
                 menu_bar: NSMenu::new(nil),
                 object: NSObj::alloc(tx).setup(),
-                pool: Cell::new(nil),
-                run_count: Cell::new(0),
-                run_mode: NSString::alloc(nil).init_str("kCFRunLoopDefaultMode"),
-                run_date: msg_send![date_cls, distantPast],
             };
-            // Don't become foreground app on launch
-            bar.app.setActivationPolicy_(NSApplicationActivationPolicyAccessory);
 
             // Default mode for menu bar items: blue highlight when selected
             msg_send![bar.status_bar_item, setHighlightMode:YES];
@@ -85,7 +64,7 @@ impl TStatusBar for OSXStatusBar {
             // See docs/icons.md for explanation of icon files.
             // TODO: Use the full list of search paths.
             let icon_name = "connectr_80px_300dpi";
-            let img_path = match bundled_resource_path(icon_name, "png") {
+            let img_path = match fruitbasket::FruitApp::bundled_resource_path(icon_name, "png") {
                 Some(path) => path,
                 None => format!("{}.png", icon_name),
             };
@@ -118,7 +97,6 @@ impl TStatusBar for OSXStatusBar {
                     cb(sender, &s.tx);
                 }
             ));
-            let _: () = msg_send![app, finishLaunching];
         }
         bar
     }
@@ -212,27 +190,11 @@ impl TStatusBar for OSXStatusBar {
         }
     }
     fn run(&mut self, block: bool) {
-        loop {
-            unsafe {
-                let run_count = self.run_count.get();
-                // Create a new release pool every once in a while, draining the old one
-                if run_count % 100 == 0 {
-                    let old_pool = self.pool.get();
-                    if run_count != 0 {
-                        let _ = msg_send![old_pool, drain];
-                    }
-                    self.pool.set(NSAutoreleasePool::new(nil));
-                }
-                let mode = self.run_mode;
-                let event: Id<Object> = msg_send![self.app, nextEventMatchingMask: -1
-                                                  untilDate: self.run_date inMode:mode dequeue: YES];
-                let _ = msg_send![self.app, sendEvent: event];
-                let _ = msg_send![self.app, updateWindows];
-                self.run_count.set(run_count + 1);
-            }
-            if !block { break; }
-            sleep(Duration::from_millis(50));
-        }
+        let period = match block {
+            true => fruitbasket::RunPeriod::Forever,
+            _ => fruitbasket::RunPeriod::Once,
+        };
+        self.app.run(period);
     }
 }
 
@@ -257,25 +219,7 @@ pub fn resource_dir() -> Option<String> {
         let cls = Class::get("NSBundle").unwrap();
         let bundle: *mut Object = msg_send![cls, mainBundle];
         let path: *mut Object = msg_send![bundle, resourcePath];
-        let cstr: *const libc::c_char = msg_send![path, UTF8String];
-        if cstr != ptr::null() {
-            let rstr = CStr::from_ptr(cstr).to_string_lossy().into_owned();
-            return Some(rstr);
-        }
-        None
-    }
-}
-
-pub fn bundled_resource_path(name: &str, extension: &str) -> Option<String> {
-    unsafe {
-        let cls = Class::get("NSBundle").unwrap();
-        let bundle: *mut Object = msg_send![cls, mainBundle];
-        let res = NSString::alloc(nil).init_str(name);
-        let ext = NSString::alloc(nil).init_str(extension);
-        let ini: *mut Object = msg_send![bundle, pathForResource:res ofType:ext];
-        let _ = msg_send![res, release];
-        let _ = msg_send![ext, release];
-        let cstr: *const libc::c_char = msg_send![ini, UTF8String];
+        let cstr: *const i8 = msg_send![path, UTF8String];
         if cstr != ptr::null() {
             let rstr = CStr::from_ptr(cstr).to_string_lossy().into_owned();
             return Some(rstr);

diff --git a/src/settings/mod.rs b/src/settings/mod.rs
line changes: +2/-10
index 7133d14..ae2a3d0
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -2,9 +2,7 @@ extern crate ini;
 use self::ini::Ini;
 
 extern crate time;
-
-#[cfg(target_os = "macos")]
-use super::osx;
+extern crate fruitbasket;
 
 use std::env;
 use std::fs;
@@ -22,19 +20,13 @@ pub struct Settings {
     pub presets: Vec<(String,String)>,
 }
 
-#[cfg(target_os = "macos")]
 fn bundled_ini() -> String {
-    match osx::bundled_resource_path("connectr", "ini") {
+    match fruitbasket::FruitApp::bundled_resource_path("connectr", "ini") {
         Some(path) => path,
         None => String::new(),
     }
 }
 
-#[cfg(not(target_os = "macos"))]
-fn bundled_ini() -> String {
-    String::new()
-}
-
 fn inifile() -> String {
     // Try to load INI file from home directory
     let path = format!("{}/.{}", env::home_dir().unwrap().display(), INIFILE);