summary history branches tags files
commit:e58d2135950f8e503f0939b85baea95a8e7cae01
author:Trevor Bentley
committer:Trevor Bentley
date:Fri Apr 21 17:48:27 2017 +0200
parents:c5ed97e84424e47a9262471cddd050bd552ab730
Exit button, OS X application bundle
diff --git a/Cargo.toml b/Cargo.toml
line changes: +10/-1
index d03ce74..baa7458
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,10 +1,18 @@
 [package]
 name = "connectr"
-version = "0.0.1"
+version = "0.0.2"
 authors = [ "Trevor Bentley <trevor@trevorbentley.com>" ]
+
+description = "Spotify Connect library and systray/menubar application for controlling Spotify devices."
+keywords = ["spotify", "connect", "spotify connect", "webapi", "systray", "menubar"]
+homepage = "https://github.com/mrmekon/connectr"
+repository = "https://github.com/mrmekon/connectr"
+license = "Apache-2.0"
+
 build = "build.rs"
 
 [lib]
+name = "connectr"
 crate-type = ["rlib", "dylib"]
 
 [profile.release]
@@ -29,6 +37,7 @@ rust-ini = "0.9"
 time = "0.1"
 timer = "0.1.6"
 chrono = "0.3.0"
+libc = "0.2"
 
 [target."cfg(windows)".dependencies]
 

diff --git a/bundle_osx.sh b/bundle_osx.sh
line changes: +36/-0
index 0000000..c13df94
--- /dev/null
+++ b/bundle_osx.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+DST="target/"
+APPDIR="Connectr.app"
+
+echo "Building OS X app..."
+
+rm -rf "$DST/$APPDIR"
+mkdir "$DST/$APPDIR/"
+mkdir "$DST/$APPDIR/Contents/"
+mkdir "$DST/$APPDIR/Contents/Resources/"
+mkdir "$DST/$APPDIR/Contents/MacOS/"
+
+cp -a target/release/connectr $DST/Connectr.app/Contents/MacOS/
+cp -a spotify.png $DST/Connectr.app/Contents/Resources/
+cp -a connectr.ini.in $DST/Connectr.app/Contents/Resources/connectr.ini
+
+strip -u -r $DST/Connectr.app/Contents/MacOS/connectr
+
+cat > "$DST/$APPDIR/Contents/Info.plist" << EOF
+{
+   CFBundleName = connectr;
+   CFBundleDisplayName = Connectr;
+   CFBundleIdentifier = "com.trevorbentley.connectr";
+   CFBundleExecutable = connectr;
+   CFBundleIconFile = "connectr.icns";
+
+   CFBundleVersion = "1.0";
+   CFBundleInfoDictionaryVersion = "6.0";
+   CFBundlePackageType = APPL;
+   CFBundleSignature = xxxx;
+
+   LSMinimumSystemVersion = "10.10.0";
+}
+EOF
+echo "Done!"

diff --git a/src/main.rs b/src/main.rs
line changes: +3/-1
index 8e2f31c..1fcecde
--- a/src/main.rs
+++ b/src/main.rs
@@ -56,8 +56,8 @@ fn play_action_label(is_playing: bool) -> &'static str {
 
 fn fill_menu<T: TStatusBar>(app: &mut ConnectrApp, spotify: &mut connectr::SpotifyConnectr, status: &mut T) {
     let device_list = spotify.request_device_list();
-
     let player_state = spotify.request_player_state();
+
     println!("Playback State:\n{}", player_state);
     let play_str = format!("{: ^50}\n{: ^50}\n{: ^50}",
                            &player_state.item.name,
@@ -183,6 +183,8 @@ fn fill_menu<T: TStatusBar>(app: &mut ConnectrApp, spotify: &mut connectr::Spoti
             i += 10;
         }
     }
+    status.add_separator();
+    status.add_quit("Exit");
 }
 
 fn clear_menu<T: TStatusBar>(app: &mut ConnectrApp, _: &mut connectr::SpotifyConnectr, status: &mut T) {

diff --git a/src/osx/mod.rs b/src/osx/mod.rs
line changes: +50/-1
index acf7e16..88ac846
--- a/src/osx/mod.rs
+++ b/src/osx/mod.rs
@@ -3,6 +3,7 @@ pub mod rustnsobject;
 extern crate objc;
 extern crate objc_foundation;
 extern crate cocoa;
+extern crate libc;
 
 pub use self::rustnsobject::NSCallback;
 
@@ -25,6 +26,8 @@ use self::rustnsobject::{NSObj, NSObjTrait, NSObjCallbackTrait};
 
 use std::sync::mpsc::Sender;
 
+use std::ptr;
+use std::ffi::CStr;
 use std::thread::sleep;
 use std::time::Duration;
 
@@ -46,6 +49,7 @@ pub trait TStatusBar {
     fn add_separator(&mut self);
     fn add_label(&mut self, label: &str);
     fn add_item(&mut self, item: &str, callback: NSCallback, selected: bool) -> *mut Object;
+    fn add_quit(&mut self, label: &str);
     fn update_item(&mut self, item: *mut Object, label: &str);
     fn sel_item(&mut self, sender: u64);
     fn unsel_item(&mut self, sender: u64);
@@ -70,8 +74,14 @@ impl TStatusBar for OSXStatusBar {
             };
             bar.app.setActivationPolicy_(NSApplicationActivationPolicyAccessory);
             msg_send![bar.status_bar_item, setHighlightMode:YES];
-            let img = NSString::alloc(nil).init_str("spotify.png");
+            let img_path = match bundled_resource_path("spotify", "png") {
+                Some(path) => path,
+                None => "spotify.png".to_string(),
+            };
+            let img = NSString::alloc(nil).init_str(&img_path);
             let icon = NSImage::alloc(nil).initWithContentsOfFile_(img);
+            //let icon = NSImage::alloc(nil).initWithContentsOfFile_(img);
+            //let icon = NSImage::imageNamed_(img, img);
             NSButton::setTitle_(bar.status_bar_item, NSString::alloc(nil).init_str("connectr"));
             bar.status_bar_item.button().setImage_(icon);
             bar.status_bar_item.setMenu_(bar.menu_bar);
@@ -106,6 +116,16 @@ impl TStatusBar for OSXStatusBar {
             self.menu_bar.addItem_(app_menu_item);
         }
     }
+    fn add_quit(&mut self, label: &str) {
+        unsafe {
+            let txt = NSString::alloc(nil).init_str(label);
+            let quit_key = NSString::alloc(nil).init_str("");
+            let app_menu_item = NSMenuItem::alloc(nil)
+                .initWithTitle_action_keyEquivalent_(txt, sel!(terminate:), quit_key)
+                .autorelease();
+            self.menu_bar.addItem_(app_menu_item);
+        }
+    }
     fn add_separator(&mut self) {
         unsafe {
             let cls = Class::get("NSMenuItem").unwrap();
@@ -171,3 +191,32 @@ impl TStatusBar for OSXStatusBar {
         }
     }
 }
+
+pub fn osx_alert(text: &str) {
+    unsafe {
+        let ns_text = NSString::alloc(nil).init_str(text);
+        let button = NSString::alloc(nil).init_str("ok");
+        let cls = Class::get("NSAlert").unwrap();
+        let alert: *mut Object = msg_send![cls, alloc];
+        let _ = msg_send![alert, init];
+        let _ = msg_send![alert, setMessageText: ns_text];
+        let _ = msg_send![alert, addButtonWithTitle: button];
+        let _ = msg_send![alert, runModal];
+    }
+}
+
+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 cstr: *const libc::c_char = msg_send![ini, UTF8String];
+        if cstr != ptr::null() {
+            let rstr = CStr::from_ptr(cstr).to_string_lossy().into_owned();
+            return Some(rstr);
+        }
+        None
+    }
+}

diff --git a/src/settings/mod.rs b/src/settings/mod.rs
line changes: +54/-3
index 0a80166..a88ae46
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -3,6 +3,15 @@ use self::ini::Ini;
 
 extern crate time;
 
+#[cfg(target_os = "macos")]
+use super::osx;
+
+use std::env;
+use std::fs;
+use std::path;
+
+const INIFILE: &'static str = ".connectr.ini";
+
 pub struct Settings {
     pub port: u32,
     pub secret: String,
@@ -13,8 +22,50 @@ pub struct Settings {
     pub presets: Vec<(String,String)>,
 }
 
+#[cfg(target_os = "macos")]
+fn bundled_ini() -> String {
+    match osx::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);
+    if path::Path::new(&path).exists() {
+        return path
+    }
+
+    // If it doesn't exist, try to copy the template from the app bundle, if
+    // such a thing exists.
+    let bundle_ini = bundled_ini();
+    if path::Path::new(&bundle_ini).exists() {
+        let _ = fs::copy(bundle_ini, path.clone());
+    }
+    path
+}
+
 pub fn read_settings() -> Option<Settings> {
-    let conf = Ini::load_from_file("connectr.ini").unwrap();
+    let conf = match Ini::load_from_file(&inifile()) {
+        Ok(c) => c,
+        Err(_) => {
+            // No connectr.ini found.  Generate a junk one in-memory, which
+            // will fail shortly after with the nice error message.
+            let mut c = Ini::new();
+            c.with_section(Some("connectr".to_owned()))
+                .set("port", 5657.to_string());
+            c.with_section(Some("application".to_owned()))
+                .set("secret", "<PLACEHOLDER>".to_string())
+                .set("client_id", "<PLACEHOLDER>".to_string());
+            c
+        }
+    };
 
     let section = conf.section(Some("connectr".to_owned())).unwrap();
     let port = section.get("port").unwrap().parse().unwrap();
@@ -59,11 +110,11 @@ pub fn read_settings() -> Option<Settings> {
 
 pub type SettingsError = String;
 pub fn save_tokens(access: &str, refresh: &str, expire_utc: u64) -> Result<(), SettingsError> {
-    let mut conf = Ini::load_from_file("connectr.ini").unwrap();
+    let mut conf = Ini::load_from_file(&inifile()).unwrap();
     conf.with_section(Some("tokens".to_owned()))
         .set("access", access)
         .set("refresh", refresh)
         .set("expire", expire_utc.to_string());
-    conf.write_to_file("connectr.ini").unwrap();
+    conf.write_to_file(&inifile()).unwrap();
     Ok(())
 }