use connectr::MenuItem;
use connectr::NSCallback;
+extern crate ctrlc;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
+
#[macro_use]
extern crate log;
extern crate log4rs;
extern crate rustc_serialize;
use rustc_serialize::json;
+use std::process;
+
// How often to refresh Spotify state (if nothing triggers a refresh earlier).
pub const REFRESH_PERIOD: i64 = 30;
data: String,
}
-#[cfg(target_os = "macos")]
-use connectr::osx;
-
struct MenuItems {
device: Vec<(MenuItem, String)>,
play: MenuItem,
}
fn fill_menu<T: TStatusBar>(app: &mut ConnectrApp, spotify: &mut connectr::SpotifyConnectr, status: &mut T) {
- app.device_list = Some(spotify.request_device_list());
- app.player_state = Some(spotify.request_player_state());
+ let dev_list = spotify.request_device_list();
+ let player_state = spotify.request_player_state();
+ match dev_list {
+ Some(_) => { app.device_list = dev_list },
+ None => { return },
+ }
+ match player_state {
+ Some(_) => { app.player_state = player_state },
+ None => { return },
+ }
let ref device_list = app.device_list.as_ref().unwrap();
let ref player_state = app.player_state.as_ref().unwrap();
require(spotify.transfer(cmd.data.clone(), true));
},
CallbackAction::PlayPause => {
- let player_state = spotify.request_player_state();
+ let fresh_player_state = spotify.request_player_state();
+ let player_state = match fresh_player_state {
+ Some(ref state) => state,
+ None => app.player_state.as_ref().unwrap(),
+ };
match player_state.is_playing {
true => {require(spotify.pause());},
false => {require(spotify.play(None));},
None => REFRESH_PERIOD,
};
let refresh_offset = std::cmp::min(REFRESH_PERIOD, refresh_offset) as i64;
- println!("State refresh in {} seconds.", refresh_offset);
+ info!("State refresh in {} seconds.", refresh_offset);
now + refresh_offset
}
+fn find_wine_path() -> Option<std::path::PathBuf> {
+ let search_paths = connectr::search_paths();
+ info!("Search paths: {:?}", search_paths);
+ for search_path in search_paths {
+ let path = std::path::PathBuf::from(search_path).join("wine");
+ if path.exists() && path.is_dir() {
+ return Some(path);
+ }
+ }
+ None
+}
+
fn main() {
create_logger();
info!("Started Connectr");
+ let running = Arc::new(AtomicBool::new(true));
+ let r = running.clone();
+ match ctrlc::set_handler(move || {
+ r.store(false, Ordering::SeqCst);
+ }) {
+ Ok(_) => {},
+ Err(_) => { error!("Failed to register Ctrl-C handler."); }
+ }
+
let mut app = ConnectrApp {
menu: MenuItems {
device: Vec::<(MenuItem, String)>::new(),
spotify.connect();
info!("Created Spotify connection.");
spotify.set_target_device(None);
- #[cfg(target_os = "macos")]
- let mut status = osx::OSXStatusBar::new(tx);
+ let mut status = connectr::StatusBar::new(tx);
info!("Created status bar.");
- #[cfg(not(target_os = "macos"))]
- let mut status = connectr::DummyStatusBar::new(tx);
- loop {
+ let mut tiny: Option<process::Child> = None;
+ if let Some(wine_dir) = find_wine_path() {
+ info!("Found wine root: {:?}", wine_dir);
+ let wine_exe = wine_dir.join("wine");
+ let tiny_exe = wine_dir.join("tiny.exe");
+ let config_dir = wine_dir.join("config");
+ debug!("{:?} / {:?} / {:?} / {:?}", wine_dir, wine_exe, config_dir, tiny_exe);
+ tiny = Some(process::Command::new(wine_exe)
+ .env("WINEPREFIX", config_dir)
+ .current_dir(wine_dir)
+ .args(&[tiny_exe])
+ .spawn().unwrap());
+ }
+ else {
+ warn!("Didn't find Wine in search path.");
+ }
+
+ while running.load(Ordering::SeqCst) {
let now = time::now_utc().to_timespec().sec as i64;
if now > refresh_time_utc {
// Redraw the whole menu once every 60 seconds, or sooner if a
status.run(false);
sleep(Duration::from_millis(10));
}
+ info!("Exiting.\n");
+ if let Some(mut tiny_proc) = tiny {
+ let _ = tiny_proc.kill();
+ let _ = tiny_proc.wait();
+ }
}
fn require(response: SpotifyResponse) {
fn new(tx: Sender<String>) -> OSXStatusBar {
let mut bar;
unsafe {
- let _ = NSAutoreleasePool::new(nil);
let app = NSApp();
let status_bar = NSStatusBar::systemStatusBar(nil);
bar = OSXStatusBar {
app: app,
- //status_bar_item: status_bar.statusItemWithLength_(NSSquareStatusItemLength),
status_bar_item: status_bar.statusItemWithLength_(NSVariableStatusItemLength),
- menu_bar: NSMenu::new(nil).autorelease(),
+ menu_bar: NSMenu::new(nil),
object: NSObj::alloc(tx).setup(),
};
bar.app.setActivationPolicy_(NSApplicationActivationPolicyAccessory);
};
let img = NSString::alloc(nil).init_str(&img_path);
let icon = NSImage::alloc(nil).initWithContentsOfFile_(img);
+ let _ = msg_send![img, release];
//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"));
+ let title = NSString::alloc(nil).init_str("connectr");
+ NSButton::setTitle_(bar.status_bar_item, title);
+ let _ = msg_send![title, release];
bar.status_bar_item.button().setImage_(icon);
+ let _ = msg_send![icon, release];
bar.status_bar_item.setMenu_(bar.menu_bar);
bar.object.cb_fn = Some(Box::new(
move |s, sender| {
cb(sender, &s.tx);
}
));
+ //unsafe { let _: () = msg_send![self.app, finishLaunching]; }
+ let _: () = msg_send![app, finishLaunching];
}
bar
}
fn clear_items(&mut self) {
unsafe {
- self.menu_bar = NSMenu::new(nil).autorelease();
+ let old_menu = self.menu_bar;
+ self.menu_bar = NSMenu::new(nil);
self.status_bar_item.setMenu_(self.menu_bar);
+ let _ = msg_send![old_menu, removeAllItems];
+ let _ = msg_send![old_menu, release];
}
}
fn set_tooltip(&self, text: &str) {
unsafe {
let img = NSString::alloc(nil).init_str(text);
let _ = msg_send![self.status_bar_item.button(), setToolTip: img];
+ let _ = msg_send![img, release];
}
}
fn add_label(&mut self, label: &str) {
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, self.object.selector(), quit_key)
- .autorelease();
+ .initWithTitle_action_keyEquivalent_(txt, self.object.selector(), quit_key);
+ let _ = msg_send![txt, release];
+ let _ = msg_send![quit_key, release];
self.menu_bar.addItem_(app_menu_item);
+ let _ = msg_send![app_menu_item, release];
}
}
fn add_quit(&mut self, label: &str) {
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();
+ .initWithTitle_action_keyEquivalent_(txt, sel!(terminate:), quit_key);
+ let _ = msg_send![txt, release];
+ let _ = msg_send![quit_key, release];
self.menu_bar.addItem_(app_menu_item);
+ let _ = msg_send![app_menu_item, release];
}
}
fn add_separator(&mut self) {
let txt = NSString::alloc(nil).init_str(item);
let quit_key = NSString::alloc(nil).init_str("");
let app_menu_item = NSMenuItem::alloc(nil)
- .initWithTitle_action_keyEquivalent_(txt, self.object.selector(), quit_key)
- .autorelease();
+ .initWithTitle_action_keyEquivalent_(txt, self.object.selector(), quit_key);
+ let _ = msg_send![txt, release];
+ let _ = msg_send![quit_key, release];
self.object.add_callback(app_menu_item, callback);
let objc = self.object.take_objc();
let _: () = msg_send![app_menu_item, setTarget: objc];
}
let item: *mut Object = app_menu_item;
self.menu_bar.addItem_(app_menu_item);
+ let _ = msg_send![app_menu_item, release];
item
}
}
unsafe {
let ns_label = NSString::alloc(nil).init_str(label);
let _: () = msg_send![item, setTitle: ns_label];
+ let _ = msg_send![ns_label, release];
}
}
fn sel_item(&mut self, sender: u64) {
}
}
fn run(&mut self, block: bool) {
- //unsafe {
- //self.app.run();
- //}
- let _ = unsafe {NSAutoreleasePool::new(nil)};
- unsafe { let _: () = msg_send![self.app, finishLaunching]; }
loop {
- sleep(Duration::from_millis(50));
unsafe {
- let _ = NSAutoreleasePool::new(nil);
+ let pool = NSAutoreleasePool::new(nil);
let cls = Class::get("NSDate").unwrap();
let date: Id<Object> = msg_send![cls, distantPast];
let mode = NSString::alloc(nil).init_str("kCFRunLoopDefaultMode");
untilDate: date inMode:mode dequeue: YES];
let _ = msg_send![self.app, sendEvent: event];
let _ = msg_send![self.app, updateWindows];
+ let _ = msg_send![mode, release];
+ let _ = msg_send![pool, drain];
}
if !block { break; }
+ sleep(Duration::from_millis(50));
}
}
}
let _ = msg_send![alert, setMessageText: ns_text];
let _ = msg_send![alert, addButtonWithTitle: button];
let _ = msg_send![alert, runModal];
+ let _ = msg_send![ns_text, release];
+ let _ = msg_send![button, release];
+ let _ = msg_send![alert, release];
+ }
+}
+
+pub fn resource_dir() -> Option<String> {
+ unsafe {
+ 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
}
}
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];
if cstr != ptr::null() {
let rstr = CStr::from_ptr(cstr).to_string_lossy().into_owned();
return path;
}
+ // Default to looking in current working directory
let path = INIFILE.to_string();
if path::Path::new(&path).exists() {
info!("Found config: {}", path);
return path;
}
- // Default to looking in current working directory
String::new()
}
&refresh_token,
self.expire_utc.unwrap());
}
- pub fn connect(&mut self) {
- if self.access_token.is_some() && !self.is_token_expired() {
- println!("Reusing saved credentials.");
- let _ = self.schedule_token_refresh();
- return ()
- }
+ pub fn authenticate(&mut self) {
println!("Requesting fresh credentials.");
self.auth_code = http::authenticate(&self.settings);
let (access_token, refresh_token, expires_in) = request_oauth_tokens(&self.auth_code, &self.settings);
self.expire_utc = Some(expire_utc);
let _ = self.schedule_token_refresh();
}
+ pub fn connect(&mut self) {
+ if self.access_token.is_some() && !self.is_token_expired() {
+ println!("Reusing saved credentials.");
+ let _ = self.schedule_token_refresh();
+ return ()
+ }
+ self.authenticate()
+ }
pub fn bearer_token(&self) -> http::AccessToken {
match self.access_token {
Some(ref x) => http::AccessToken::Bearer(x),
let (access_token, _, expires_in) = parse_spotify_token(&json_response);
(access_token, expires_in)
}
- pub fn request_device_list(&self) -> ConnectDeviceList {
+ pub fn request_device_list(&mut self) -> Option<ConnectDeviceList> {
let json_response = http::http(spotify_api::DEVICES, "", "",
- http::HttpMethod::GET, self.bearer_token()).unwrap();
- json::decode(&json_response).unwrap()
+ http::HttpMethod::GET, self.bearer_token());
+ match json_response.code {
+ Some(200) => Some(json::decode(&json_response.data.unwrap()).unwrap()),
+ Some(401) => {
+ self.authenticate();
+ None
+ }
+ _ => None
+ }
}
- pub fn request_player_state(&self) -> PlayerState {
+ pub fn request_player_state(&mut self) -> Option<PlayerState> {
let json_response = http::http(spotify_api::PLAYER_STATE, "", "",
- http::HttpMethod::GET, self.bearer_token()).unwrap();
- json::decode(&json_response).unwrap()
+ http::HttpMethod::GET, self.bearer_token());
+ match json_response.code {
+ Some(200) => Some(json::decode(&json_response.data.unwrap()).unwrap()),
+ Some(401) => {
+ self.authenticate();
+ None
+ }
+ _ => None
+ }
}
pub fn set_target_device(&mut self, device: Option<DeviceId>) {
self.device = device;