<commit_before>#[macro_use]
extern crate log;
extern crate env_logger;
extern crate irc;
#[macro_use]
extern crate lazy_static;
extern crate regex;
extern crate hyper;
extern crate hubcaps;
extern crate hyper_native_tls;
extern crate serde_json;

use std::env;
use std::fmt;
use std::thread;
use std::collections::HashMap;
use regex::Regex;

use irc::client::prelude::*;
use irc::client::data::command::Command;

use hyper::Client;
use hyper::net::HttpsConnector;
use hyper_native_tls::NativeTlsClient;
use hubcaps::{Credentials, Github};
use hubcaps::comments::CommentOptions;

fn main() {
    env_logger::init().unwrap();

    let config_file =
        {
            let mut args = env::args_os();
            args.next().expect("What, no program name?");
            let config_file = args.next().expect("Expected a single command-line argument, the JSON configuration file.");
            if args.next().is_some() {
                panic!("Expected only a single command-line argument, the JSON configuration file.");
            }
            config_file
        };

    let server = IrcServer::new(config_file).expect("Couldn't initialize server with given configuration file");
    server.identify().unwrap();

    let options = server
        .config()
        .options
        .as_ref()
        .expect("No options property within configuration?");

    // FIXME: Add options to ask the bot which channels it's in, and
    // which channels it currently has buffers in.  (Then we can do
    // things like ask the bot to reboot itself, but it will only do so
    // if it's not busy.)
    let mut channel_data: HashMap<String, ChannelData> = HashMap::new();

    for message in server.iter() {
        let message = message.unwrap(); // panic if there's an error

        match message.command {
            Command::PRIVMSG(ref target, ref msg) => {
                match message.source_nickname() {
                    None => {
                        warn!("PRIVMSG without a source! {}",
                              format!("{}", message).trim());
                    }
                    Some(ref source) => {
                        let source_ = String::from(*source);
                        let line = if msg.starts_with("\x01ACTION ") && msg.ends_with("\x01") {
                            ChannelLine {
                                source: source_,
                                is_action: true,
                                message: String::from(&msg[8..msg.len() - 1]),
                            }
                        } else {
                            ChannelLine {
                                source: source_,
                                is_action: false,
                                message: msg.clone(),
                            }
                        };
                        let mynick = server.current_nickname();
                        if target == mynick {
                            // An actual private message.
                            info!("[{}] {}", source, line);
                            handle_bot_command(&server,
                                               options,
                                               None,
                                               &line.message,
                                               source,
                                               false,
                                               None)
                        } else if target.starts_with('#') {
                            // A message in a channel.
                            info!("[{}] {}", target, line);
                            let this_channel_data =
                                channel_data
                                    .entry(target.clone())
                                    .or_insert_with(|| ChannelData::new(&options));
                            match check_command_in_channel(mynick, &line.message) {
                                Some(ref command) => {
                                    handle_bot_command(&server,
                                                       options,
                                                       Some(this_channel_data),
                                                       command,
                                                       target,
                                                       line.is_action,
                                                       Some(source))
                                }
                                None => {
                                    if let Some(response) = this_channel_data.add_line(line) {
                                        server.send_privmsg(target, &*response).unwrap();
                                    }
                                }
                            }
                        } else {
                            warn!("UNEXPECTED TARGET {} in message {}",
                                  target,
                                  format!("{}", message).trim());
                        }
                    }
                }
            }
            Command::INVITE(ref target, ref channel) => {
                if target == server.current_nickname() {
                    // Join channels when invited.
                    server.send_join(channel).unwrap();
                }
            }
            _ => (),
        }
    }
}

// Take a message in the channel, and see if it was a message sent to
// this bot.
fn check_command_in_channel(mynick: &str, msg: &String) -> Option<String> {
    if !msg.starts_with(mynick) {
        return None;
    }
    let after_nick = &msg[mynick.len()..];
    if !after_nick.starts_with(":") && !after_nick.starts_with(",") {
        return None;
    }
    let after_punct = &after_nick[1..];
    Some(String::from(after_punct.trim_left()))
}

fn handle_bot_command(server: &IrcServer,
                      options: &HashMap<String, String>,
                      this_channel_data: Option<&mut ChannelData>,
                      command: &str,
                      response_target: &str,
                      response_is_action: bool,
                      response_username: Option<&str>) {

    let send_line = |response_username: Option<&str>, line: &str| {
        let line_with_nick = match response_username {
            None => String::from(line),
            Some(username) => String::from(username) + ", " + line,
        };
        let adjusted_line = if response_is_action {
            format!("\x01ACTION {}\x01", line_with_nick)
        } else {
            line_with_nick
        };
        server
            .send_privmsg(response_target, &adjusted_line)
            .unwrap();
    };

    if command == "help" {
        send_line(response_username, "The commands I understand are:");
        send_line(None, "  help     Send this message.");
        send_line(None, "  intro    Send a message describing what I do.");
        send_line(None,
                  "  bye      Leave the channel.  (You can /invite me back.)");
        return;
    }

    if command == "intro" {
        let config = server.config();
        send_line(None,
                  "My job is to leave comments in github when the group discusses github issues and takes minutes in IRC.");
        send_line(None,
                  "I separate discussions by the \"Topic:\" lines, and I know what github issues to use only by lines of the form \"Topic github: <url>\", or if the \"Topic:\" line is a github URL.");
        send_line(None,
                  &*format!("I'm only allowed to comment on issues in the repositories: {}.",
                            options["github_repos_allowed"]));
        let owners = if let Some(v) = config.owners.as_ref() {
            v.join(" ")
        } else {
            String::from("")
        };
        send_line(None,
                  &*format!("My source code is at {} and I'm run by {}.",
                            options["source"],
                            owners));
        return;
    }

    if command == "bye" {
        match this_channel_data {
            None => {
                send_line(response_username, "'bye' only works in a channel");
            }
            Some(this_channel_data) => {
                this_channel_data.end_topic();
                server
                    .send(Command::PART(String::from(response_target),
                                        Some(format!("Leaving at request of {}.  Feel free to /invite me back.",
                                                     response_username.unwrap()))))
                    .unwrap();
            }
        }
        return;
    }

    send_line(response_username,
              "Sorry, I don't understand that command.  Try 'help'.");
}

struct ChannelLine {
    source: String,
    is_action: bool,
    message: String,
}

struct TopicData {
    topic: String,
    github_url: Option<String>,
    lines: Vec<ChannelLine>,
    resolutions: Vec<String>,
}

struct ChannelData<'opts> {
    current_topic: Option<TopicData>,
    options: &'opts HashMap<String, String>,
}

impl fmt::Display for ChannelLine {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.is_action {
            write!(f, "* {} {}", self.source, self.message)
        } else {
            write!(f, "<{}> {}", self.source, self.message)
        }
    }
}

impl TopicData {
    fn new(topic: &str) -> TopicData {
        let topic_ = String::from(topic);
        TopicData {
            topic: topic_,
            github_url: None,
            lines: vec![],
            resolutions: vec![],
        }
    }
}

impl fmt::Display for TopicData {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.resolutions.len() == 0 {
            try!(write!(f,
                        "The CSS Working Group just discussed {}.\n\n",
                        self.topic));
        } else {
            try!(write!(f,
                        "The CSS Working Group just discussed {}, and agreed to the following resolutions:\n\n",
                        self.topic));
            for resolution in &self.resolutions {
                try!(write!(f, "```\n{}\n```\n\n", resolution));
            }
        }

        try!(write!(f,
                    "The full IRC log of that discussion\n"));
        try!(write!(f, "\n```\n"));
        for line in &self.lines {
            try!(write!(f, "{}\n", line));
        }
        try!(write!(f, "```\n\n"));
        Ok(())
    }
}

/// A case-insensitive version of starts_with.
fn ci_starts_with(s: &str, prefix: &str) -> bool {
    assert!(prefix.to_lowercase() == prefix);

    s.len() >= prefix.len() && s[0..prefix.len()].to_lowercase() == prefix
}

/// Remove a case-insensitive start of the line, and if that prefix is
/// present return the rest of the line.
fn strip_ci_prefix(s: &str, prefix: &str) -> Option<String> {
    if ci_starts_with(s, prefix) {
        Some(String::from(s[prefix.len()..].trim_left()))
    } else {
        None
    }
}

impl<'opts> ChannelData<'opts> {
    fn new(options_: &'opts HashMap<String, String>) -> ChannelData {
        ChannelData {
            current_topic: None,
            options: options_,
        }
    }

    // Returns the response that should be sent to the message over IRC.
    fn add_line(&mut self, line: ChannelLine) -> Option<String> {
        if let Some(ref topic) = strip_ci_prefix(&line.message, "topic:") {
            self.start_topic(topic);
        }
        if line.source == "trackbot" && line.is_action == true &&
           line.message == "is ending a teleconference." {
            self.end_topic();
        }
        match self.current_topic {
            None => None,
            Some(ref mut data) => {
                let new_url_option = extract_github_url(&line.message, self.options);
                let response = match (new_url_option.as_ref(), data.github_url.as_ref()) {
                    (None, _) => None,
                    // FIXME: Add and implement instructions to cancel!
                    (Some(new_url), None) => {
                        Some(format!("OK, I'll post this discussion to {}", new_url))
                    }
                    (Some(new_url), Some(old_url)) if old_url == new_url => None,
                    (Some(new_url), Some(old_url)) => {
                        Some(format!("OK, I'll post this discussion to {} instead of {} like you said before",
                                     new_url,
                                     old_url))
                    }
                };

                if let Some(new_url) = new_url_option {
                    data.github_url = Some(new_url);
                }

                if line.message.starts_with("RESOLUTION") || line.message.starts_with("RESOLVED") {
                    data.resolutions.push(line.message.clone());
                }

                data.lines.push(line);

                response
            }
        }
    }

    fn start_topic(&mut self, topic: &str) {
        self.end_topic();
        self.current_topic = Some(TopicData::new(topic));
    }

    fn end_topic(&mut self) {
        // TODO: Test the topic boundary code.
        if let Some(topic) = self.current_topic.take() {
            if topic.github_url.is_some() {
                let task = GithubCommentTask::new(topic, self.options);
                task.run();
            }
        }
    }
}

fn extract_github_url(message: &str, options: &HashMap<String, String>) -> Option<String> {
    lazy_static! {
        static ref GITHUB_URL_RE: Regex =
            Regex::new(r"^https://github.com/(?P<repo>[^/]*/[^/]*)/issues/(?P<number>[0-9]+)$")
            .unwrap();
    }
    let ref allowed_repos = options["github_repos_allowed"];
    for prefix in ["topic:", "github topic:"].into_iter() {
        if let Some(ref maybe_url) = strip_ci_prefix(&message, prefix) {
            if let Some(ref caps) = GITHUB_URL_RE.captures(maybe_url) {
                for repo in allowed_repos.split_whitespace() {
                    if caps["repo"] == *repo {
                        return Some(maybe_url.clone());
                    }
                }
            }
        }
    }
    None
}

struct GithubCommentTask {
    data: TopicData,
    github: Github,
}

impl GithubCommentTask {
    fn new(data_: TopicData, options: &HashMap<String, String>) -> GithubCommentTask {
        let github_ =
            Github::new(&*options["github_uastring"],
                        Client::with_connector(HttpsConnector::new(NativeTlsClient::new()
                                                                       .unwrap())),
                        Credentials::Token(options["github_access_token"].clone()));
        GithubCommentTask {
            data: data_,
            github: github_,
        }
    }
    fn run(self) {
        thread::spawn(move || { self.main(); });
    }
    fn main(&self) {
        lazy_static! {
            static ref GITHUB_URL_RE: Regex =
                Regex::new(r"^https://github.com/(?P<owner>[^/]*)/(?P<repo>[^/]*)/issues/(?P<number>[0-9]+)$")
                .unwrap();
        }

        if let Some(ref github_url) = self.data.github_url {
            if let Some(ref caps) = GITHUB_URL_RE.captures(github_url) {
                let repo = self.github
                    .repo(String::from(&caps["owner"]), String::from(&caps["repo"]));
                let issue = repo.issue(caps["number"].parse::<u64>().unwrap());
                let comments = issue.comments();

                let comment_text = format!("{}", self.data);
                comments
                    .create(&CommentOptions { body: comment_text })
                    .unwrap();
            } else {
                warn!("How does {} fail to match now when it matched before?",
                      github_url)
            }
        }
    }
}
<commit_msg>Ever so slightly simpler argument handling.<commit_after>#[macro_use]
extern crate log;
extern crate env_logger;
extern crate irc;
#[macro_use]
extern crate lazy_static;
extern crate regex;
extern crate hyper;
extern crate hubcaps;
extern crate hyper_native_tls;
extern crate serde_json;

use std::env;
use std::fmt;
use std::thread;
use std::collections::HashMap;
use regex::Regex;

use irc::client::prelude::*;
use irc::client::data::command::Command;

use hyper::Client;
use hyper::net::HttpsConnector;
use hyper_native_tls::NativeTlsClient;
use hubcaps::{Credentials, Github};
use hubcaps::comments::CommentOptions;

fn main() {
    env_logger::init().unwrap();

    let config_file =
        {
            let mut args = env::args_os().skip(1); // skip program name
            let config_file = args.next().expect("Expected a single command-line argument, the JSON configuration file.");
            if args.next().is_some() {
                panic!("Expected only a single command-line argument, the JSON configuration file.");
            }
            config_file
        };

    let server = IrcServer::new(config_file).expect("Couldn't initialize server with given configuration file");
    server.identify().unwrap();

    let options = server
        .config()
        .options
        .as_ref()
        .expect("No options property within configuration?");

    // FIXME: Add options to ask the bot which channels it's in, and
    // which channels it currently has buffers in.  (Then we can do
    // things like ask the bot to reboot itself, but it will only do so
    // if it's not busy.)
    let mut channel_data: HashMap<String, ChannelData> = HashMap::new();

    for message in server.iter() {
        let message = message.unwrap(); // panic if there's an error

        match message.command {
            Command::PRIVMSG(ref target, ref msg) => {
                match message.source_nickname() {
                    None => {
                        warn!("PRIVMSG without a source! {}",
                              format!("{}", message).trim());
                    }
                    Some(ref source) => {
                        let source_ = String::from(*source);
                        let line = if msg.starts_with("\x01ACTION ") && msg.ends_with("\x01") {
                            ChannelLine {
                                source: source_,
                                is_action: true,
                                message: String::from(&msg[8..msg.len() - 1]),
                            }
                        } else {
                            ChannelLine {
                                source: source_,
                                is_action: false,
                                message: msg.clone(),
                            }
                        };
                        let mynick = server.current_nickname();
                        if target == mynick {
                            // An actual private message.
                            info!("[{}] {}", source, line);
                            handle_bot_command(&server,
                                               options,
                                               None,
                                               &line.message,
                                               source,
                                               false,
                                               None)
                        } else if target.starts_with('#') {
                            // A message in a channel.
                            info!("[{}] {}", target, line);
                            let this_channel_data =
                                channel_data
                                    .entry(target.clone())
                                    .or_insert_with(|| ChannelData::new(&options));
                            match check_command_in_channel(mynick, &line.message) {
                                Some(ref command) => {
                                    handle_bot_command(&server,
                                                       options,
                                                       Some(this_channel_data),
                                                       command,
                                                       target,
                                                       line.is_action,
                                                       Some(source))
                                }
                                None => {
                                    if let Some(response) = this_channel_data.add_line(line) {
                                        server.send_privmsg(target, &*response).unwrap();
                                    }
                                }
                            }
                        } else {
                            warn!("UNEXPECTED TARGET {} in message {}",
                                  target,
                                  format!("{}", message).trim());
                        }
                    }
                }
            }
            Command::INVITE(ref target, ref channel) => {
                if target == server.current_nickname() {
                    // Join channels when invited.
                    server.send_join(channel).unwrap();
                }
            }
            _ => (),
        }
    }
}

// Take a message in the channel, and see if it was a message sent to
// this bot.
fn check_command_in_channel(mynick: &str, msg: &String) -> Option<String> {
    if !msg.starts_with(mynick) {
        return None;
    }
    let after_nick = &msg[mynick.len()..];
    if !after_nick.starts_with(":") && !after_nick.starts_with(",") {
        return None;
    }
    let after_punct = &after_nick[1..];
    Some(String::from(after_punct.trim_left()))
}

fn handle_bot_command(server: &IrcServer,
                      options: &HashMap<String, String>,
                      this_channel_data: Option<&mut ChannelData>,
                      command: &str,
                      response_target: &str,
                      response_is_action: bool,
                      response_username: Option<&str>) {

    let send_line = |response_username: Option<&str>, line: &str| {
        let line_with_nick = match response_username {
            None => String::from(line),
            Some(username) => String::from(username) + ", " + line,
        };
        let adjusted_line = if response_is_action {
            format!("\x01ACTION {}\x01", line_with_nick)
        } else {
            line_with_nick
        };
        server
            .send_privmsg(response_target, &adjusted_line)
            .unwrap();
    };

    if command == "help" {
        send_line(response_username, "The commands I understand are:");
        send_line(None, "  help     Send this message.");
        send_line(None, "  intro    Send a message describing what I do.");
        send_line(None,
                  "  bye      Leave the channel.  (You can /invite me back.)");
        return;
    }

    if command == "intro" {
        let config = server.config();
        send_line(None,
                  "My job is to leave comments in github when the group discusses github issues and takes minutes in IRC.");
        send_line(None,
                  "I separate discussions by the \"Topic:\" lines, and I know what github issues to use only by lines of the form \"Topic github: <url>\", or if the \"Topic:\" line is a github URL.");
        send_line(None,
                  &*format!("I'm only allowed to comment on issues in the repositories: {}.",
                            options["github_repos_allowed"]));
        let owners = if let Some(v) = config.owners.as_ref() {
            v.join(" ")
        } else {
            String::from("")
        };
        send_line(None,
                  &*format!("My source code is at {} and I'm run by {}.",
                            options["source"],
                            owners));
        return;
    }

    if command == "bye" {
        match this_channel_data {
            None => {
                send_line(response_username, "'bye' only works in a channel");
            }
            Some(this_channel_data) => {
                this_channel_data.end_topic();
                server
                    .send(Command::PART(String::from(response_target),
                                        Some(format!("Leaving at request of {}.  Feel free to /invite me back.",
                                                     response_username.unwrap()))))
                    .unwrap();
            }
        }
        return;
    }

    send_line(response_username,
              "Sorry, I don't understand that command.  Try 'help'.");
}

struct ChannelLine {
    source: String,
    is_action: bool,
    message: String,
}

struct TopicData {
    topic: String,
    github_url: Option<String>,
    lines: Vec<ChannelLine>,
    resolutions: Vec<String>,
}

struct ChannelData<'opts> {
    current_topic: Option<TopicData>,
    options: &'opts HashMap<String, String>,
}

impl fmt::Display for ChannelLine {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.is_action {
            write!(f, "* {} {}", self.source, self.message)
        } else {
            write!(f, "<{}> {}", self.source, self.message)
        }
    }
}

impl TopicData {
    fn new(topic: &str) -> TopicData {
        let topic_ = String::from(topic);
        TopicData {
            topic: topic_,
            github_url: None,
            lines: vec![],
            resolutions: vec![],
        }
    }
}

impl fmt::Display for TopicData {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.resolutions.len() == 0 {
            try!(write!(f,
                        "The CSS Working Group just discussed {}.\n\n",
                        self.topic));
        } else {
            try!(write!(f,
                        "The CSS Working Group just discussed {}, and agreed to the following resolutions:\n\n",
                        self.topic));
            for resolution in &self.resolutions {
                try!(write!(f, "```\n{}\n```\n\n", resolution));
            }
        }

        try!(write!(f,
                    "The full IRC log of that discussion\n"));
        try!(write!(f, "\n```\n"));
        for line in &self.lines {
            try!(write!(f, "{}\n", line));
        }
        try!(write!(f, "```\n\n"));
        Ok(())
    }
}

/// A case-insensitive version of starts_with.
fn ci_starts_with(s: &str, prefix: &str) -> bool {
    assert!(prefix.to_lowercase() == prefix);

    s.len() >= prefix.len() && s[0..prefix.len()].to_lowercase() == prefix
}

/// Remove a case-insensitive start of the line, and if that prefix is
/// present return the rest of the line.
fn strip_ci_prefix(s: &str, prefix: &str) -> Option<String> {
    if ci_starts_with(s, prefix) {
        Some(String::from(s[prefix.len()..].trim_left()))
    } else {
        None
    }
}

impl<'opts> ChannelData<'opts> {
    fn new(options_: &'opts HashMap<String, String>) -> ChannelData {
        ChannelData {
            current_topic: None,
            options: options_,
        }
    }

    // Returns the response that should be sent to the message over IRC.
    fn add_line(&mut self, line: ChannelLine) -> Option<String> {
        if let Some(ref topic) = strip_ci_prefix(&line.message, "topic:") {
            self.start_topic(topic);
        }
        if line.source == "trackbot" && line.is_action == true &&
           line.message == "is ending a teleconference." {
            self.end_topic();
        }
        match self.current_topic {
            None => None,
            Some(ref mut data) => {
                let new_url_option = extract_github_url(&line.message, self.options);
                let response = match (new_url_option.as_ref(), data.github_url.as_ref()) {
                    (None, _) => None,
                    // FIXME: Add and implement instructions to cancel!
                    (Some(new_url), None) => {
                        Some(format!("OK, I'll post this discussion to {}", new_url))
                    }
                    (Some(new_url), Some(old_url)) if old_url == new_url => None,
                    (Some(new_url), Some(old_url)) => {
                        Some(format!("OK, I'll post this discussion to {} instead of {} like you said before",
                                     new_url,
                                     old_url))
                    }
                };

                if let Some(new_url) = new_url_option {
                    data.github_url = Some(new_url);
                }

                if line.message.starts_with("RESOLUTION") || line.message.starts_with("RESOLVED") {
                    data.resolutions.push(line.message.clone());
                }

                data.lines.push(line);

                response
            }
        }
    }

    fn start_topic(&mut self, topic: &str) {
        self.end_topic();
        self.current_topic = Some(TopicData::new(topic));
    }

    fn end_topic(&mut self) {
        // TODO: Test the topic boundary code.
        if let Some(topic) = self.current_topic.take() {
            if topic.github_url.is_some() {
                let task = GithubCommentTask::new(topic, self.options);
                task.run();
            }
        }
    }
}

fn extract_github_url(message: &str, options: &HashMap<String, String>) -> Option<String> {
    lazy_static! {
        static ref GITHUB_URL_RE: Regex =
            Regex::new(r"^https://github.com/(?P<repo>[^/]*/[^/]*)/issues/(?P<number>[0-9]+)$")
            .unwrap();
    }
    let ref allowed_repos = options["github_repos_allowed"];
    for prefix in ["topic:", "github topic:"].into_iter() {
        if let Some(ref maybe_url) = strip_ci_prefix(&message, prefix) {
            if let Some(ref caps) = GITHUB_URL_RE.captures(maybe_url) {
                for repo in allowed_repos.split_whitespace() {
                    if caps["repo"] == *repo {
                        return Some(maybe_url.clone());
                    }
                }
            }
        }
    }
    None
}

struct GithubCommentTask {
    data: TopicData,
    github: Github,
}

impl GithubCommentTask {
    fn new(data_: TopicData, options: &HashMap<String, String>) -> GithubCommentTask {
        let github_ =
            Github::new(&*options["github_uastring"],
                        Client::with_connector(HttpsConnector::new(NativeTlsClient::new()
                                                                       .unwrap())),
                        Credentials::Token(options["github_access_token"].clone()));
        GithubCommentTask {
            data: data_,
            github: github_,
        }
    }
    fn run(self) {
        thread::spawn(move || { self.main(); });
    }
    fn main(&self) {
        lazy_static! {
            static ref GITHUB_URL_RE: Regex =
                Regex::new(r"^https://github.com/(?P<owner>[^/]*)/(?P<repo>[^/]*)/issues/(?P<number>[0-9]+)$")
                .unwrap();
        }

        if let Some(ref github_url) = self.data.github_url {
            if let Some(ref caps) = GITHUB_URL_RE.captures(github_url) {
                let repo = self.github
                    .repo(String::from(&caps["owner"]), String::from(&caps["repo"]));
                let issue = repo.issue(caps["number"].parse::<u64>().unwrap());
                let comments = issue.comments();

                let comment_text = format!("{}", self.data);
                comments
                    .create(&CommentOptions { body: comment_text })
                    .unwrap();
            } else {
                warn!("How does {} fail to match now when it matched before?",
                      github_url)
            }
        }
    }
}
<|endoftext|>


<commit_before>#![crate_name = "rboy"]

extern crate clap;
extern crate cpal;
extern crate glium;
extern crate rboy;

use glium::DisplayBuild;
use rboy::device::Device;
use std::sync::mpsc::{self, Receiver, SyncSender, TryRecvError, TrySendError};
use std::thread;
use std::error::Error;

const EXITCODE_SUCCESS : i32 = 0;
const EXITCODE_CPULOADFAILS : i32 = 2;

#[derive(Default)]
struct RenderOptions {
    pub linear_interpolation: bool,
}

enum GBEvent {
    KeyUp(rboy::KeypadKey),
    KeyDown(rboy::KeypadKey),
    SpeedUp,
    SpeedDown,
}

fn main() {
    let exit_status = real_main();
    if exit_status != EXITCODE_SUCCESS {
        std::process::exit(exit_status);
    }
}

fn real_main() -> i32 {
    let matches = clap::App::new("rboy")
        .version("0.1")
        .author("Mathijs van de Nes")
        .about("A Gameboy Colour emulator written in Rust")
        .arg(clap::Arg::with_name("filename")
             .help("Sets the ROM file to load")
             .required(true))
        .arg(clap::Arg::with_name("serial")
             .help("Prints the data from the serial port to stdout")
             .short("s")
             .long("serial"))
        .arg(clap::Arg::with_name("classic")
             .help("Forces the emulator to run in classic Gameboy mode")
             .short("c")
             .long("classic"))
        .arg(clap::Arg::with_name("scale")
             .help("Sets the scale of the interface. Default: 2")
             .short("x")
             .long("scale")
             .validator(|s|
                 match s.parse::<u32>() {
                     Err(e) => Err(format!("Could not parse scale: {}", e.description())),
                     Ok(s) if s < 1 => Err("Scale must be at least 1".to_owned()),
                     Ok(s) if s > 8 => Err("Scale may be at most 8".to_owned()),
                     Ok(..) => Ok(()),
                 })
             .takes_value(true))
        .arg(clap::Arg::with_name("audio")
             .help("Enables audio")
             .short("a")
             .long("audio"))
        .get_matches();

    let opt_serial = matches.is_present("serial");
    let opt_classic = matches.is_present("classic");
    let opt_audio = matches.is_present("audio");
    let filename = matches.value_of("filename").unwrap();
    let scale = matches.value_of("scale").unwrap_or("2").parse::<u32>().unwrap();

    let cpu = construct_cpu(filename, opt_classic, opt_serial);
    if cpu.is_none() { return EXITCODE_CPULOADFAILS; }
    let mut cpu = cpu.unwrap();
    if opt_audio {
        let player = CpalPlayer::get();
        match player {
            Some(v) => cpu.enable_audio(Box::new(v) as Box<rboy::AudioPlayer>),
            None => { warn("Could not open audio device"); return EXITCODE_CPULOADFAILS; },
        }
    }
    let romname = cpu.romname();

    let (sender1, receiver1) = mpsc::channel();
    let (sender2, receiver2) = mpsc::sync_channel(1);

    let display = glium::glutin::WindowBuilder::new()
        .with_dimensions(rboy::SCREEN_W as u32 * scale, rboy::SCREEN_H as u32 * scale)
        .with_title("RBoy - ".to_owned() + &romname)
        .build_glium()
        .unwrap();

    let mut texture = glium::texture::texture2d::Texture2d::empty_with_format(
            &display,
            glium::texture::UncompressedFloatFormat::U8U8U8,
            glium::texture::MipmapsOption::NoMipmap,
            rboy::SCREEN_W as u32,
            rboy::SCREEN_H as u32)
        .unwrap();

    let mut renderoptions = <RenderOptions as Default>::default();

    let cputhread = thread::spawn(move|| run_cpu(cpu, sender2, receiver1));

    'main : loop {
        for ev in display.poll_events() {
            use glium::glutin::Event;
            use glium::glutin::ElementState::{Pressed, Released};
            use glium::glutin::VirtualKeyCode;

            match ev {
                Event::Closed
                    => break 'main,
                Event::KeyboardInput(Pressed, _, Some(VirtualKeyCode::Escape))
                    => break 'main,
                Event::KeyboardInput(Pressed, _, Some(VirtualKeyCode::Key1))
                    => display.get_window().unwrap().set_inner_size(rboy::SCREEN_W as u32, rboy::SCREEN_H as u32),
                Event::KeyboardInput(Pressed, _, Some(VirtualKeyCode::R))
                    => display.get_window().unwrap().set_inner_size(rboy::SCREEN_W as u32 * scale, rboy::SCREEN_H as u32 * scale),
                Event::KeyboardInput(Pressed, _, Some(VirtualKeyCode::LShift))
                    => { let _ = sender1.send(GBEvent::SpeedUp); },
                Event::KeyboardInput(Released, _, Some(VirtualKeyCode::LShift))
                    => { let _ = sender1.send(GBEvent::SpeedDown); },
                Event::KeyboardInput(Pressed, _, Some(VirtualKeyCode::T))
                    => { renderoptions.linear_interpolation = !renderoptions.linear_interpolation; }
                Event::KeyboardInput(Pressed, _, Some(glutinkey)) => {
                    if let Some(key) = glutin_to_keypad(glutinkey) {
                        let _ = sender1.send(GBEvent::KeyDown(key));
                    }
                },
                Event::KeyboardInput(Released, _, Some(glutinkey)) => {
                    if let Some(key) = glutin_to_keypad(glutinkey) {
                        let _ = sender1.send(GBEvent::KeyUp(key));
                    }
                },
                _ => (),
            }
        }

        match receiver2.try_recv() {
            Ok(data) => recalculate_screen(&display, &mut texture, &*data, &renderoptions),
            Err(TryRecvError::Empty) => (),
            Err(TryRecvError::Disconnected) => break 'main,
        }
    }

    drop(sender1);
    let _ = cputhread.join();

    EXITCODE_SUCCESS
}

fn glutin_to_keypad(key: glium::glutin::VirtualKeyCode) -> Option<rboy::KeypadKey> {
    use glium::glutin::VirtualKeyCode;
    match key {
        VirtualKeyCode::Z => Some(rboy::KeypadKey::A),
        VirtualKeyCode::X => Some(rboy::KeypadKey::B),
        VirtualKeyCode::Up => Some(rboy::KeypadKey::Up),
        VirtualKeyCode::Down => Some(rboy::KeypadKey::Down),
        VirtualKeyCode::Left => Some(rboy::KeypadKey::Left),
        VirtualKeyCode::Right => Some(rboy::KeypadKey::Right),
        VirtualKeyCode::Space => Some(rboy::KeypadKey::Select),
        VirtualKeyCode::Return => Some(rboy::KeypadKey::Start),
        _ => None,
    }
}

fn recalculate_screen(display: &glium::backend::glutin_backend::GlutinFacade,
                      texture: &mut glium::texture::texture2d::Texture2d,
                      datavec: &[u8],
                      renderoptions: &RenderOptions)
{
    use glium::Surface;

    let interpolation_type = if renderoptions.linear_interpolation {
        glium::uniforms::MagnifySamplerFilter::Linear
    }
    else {
        glium::uniforms::MagnifySamplerFilter::Nearest
    };

    let rawimage2d = glium::texture::RawImage2d {
        data: std::borrow::Cow::Borrowed(datavec),
        width: rboy::SCREEN_W as u32,
        height: rboy::SCREEN_H as u32,
        format: glium::texture::ClientFormat::U8U8U8,
    };
    texture.write(
        glium::Rect {
            left: 0,
            bottom: 0,
            width: rboy::SCREEN_W as u32,
            height: rboy::SCREEN_H as u32
        },
        rawimage2d);

    // We use a custom BlitTarget to transform OpenGL coordinates to row-column coordinates
    let target = display.draw();
    let (target_w, target_h) = target.get_dimensions();
    texture.as_surface().blit_whole_color_to(
        &target,
        &glium::BlitTarget {
            left: 0,
            bottom: target_h,
            width: target_w as i32,
            height: -(target_h as i32)
        },
        interpolation_type);
    target.finish().unwrap();
}

fn warn(message: &'static str) {
    use std::io::Write;
    let _ = write!(&mut std::io::stderr(), "{}\n", message);
}

fn construct_cpu(filename: &str, classic_mode: bool, output_serial: bool) -> Option<Device> {
    let opt_c = match classic_mode {
        true => Device::new(filename),
        false => Device::new_cgb(filename),
    };
    let mut c = match opt_c
    {
        Ok(cpu) => { cpu },
        Err(message) => { warn(message); return None; },
    };
    c.set_stdout(output_serial);
    Some(c)
}

fn run_cpu(mut cpu: Device, sender: SyncSender<Vec<u8>>, receiver: Receiver<GBEvent>) {
    let periodic = timer_periodic(16);
    let mut limit_speed = true;

    let waitticks = (4194304f64 / 1000.0 * 16.0).round() as u32;
    let mut ticks = 0;

    'outer: loop {
        while ticks < waitticks {
            ticks += cpu.do_cycle();
            if cpu.check_and_reset_gpu_updated() {
                let data = cpu.get_gpu_data().to_vec();
                if let Err(TrySendError::Disconnected(..)) = sender.try_send(data) {
                    break 'outer;
                }
            }
        }

        ticks -= waitticks;

        'recv: loop {
            match receiver.try_recv() {
                Ok(event) => {
                    match event {
                        GBEvent::KeyUp(key) => cpu.keyup(key),
                        GBEvent::KeyDown(key) => cpu.keydown(key),
                        GBEvent::SpeedUp => limit_speed = false,
                        GBEvent::SpeedDown => { limit_speed = true; cpu.sync_audio(); }
                    }
                },
                Err(TryRecvError::Empty) => break 'recv,
                Err(TryRecvError::Disconnected) => break 'outer,
            }
        }

        if limit_speed { let _ = periodic.recv(); }
    }
}

fn timer_periodic(ms: u64) -> Receiver<()> {
    let (tx, rx) = std::sync::mpsc::sync_channel(1);
    std::thread::spawn(move || {
        loop {
            std::thread::sleep(std::time::Duration::from_millis(ms));
            if tx.send(()).is_err() {
                break;
            }
        }
    });
    rx
}

struct CpalPlayer {
    voice: cpal::Voice,
}

impl CpalPlayer {
    fn get() -> Option<CpalPlayer> {
        if cpal::get_endpoints_list().count() == 0 { return None; }

        let endpoint = match cpal::get_default_endpoint() {
            Some(e) => e,
            None => return None,
        };

        let mut bestf = None;
        for f in endpoint.get_supported_formats_list().unwrap() {
            if bestf.is_none() {
                bestf = Some(f);
            }
            else if f.channels.len() == 2 && f.samples_rate.0 >= 192000 && f.data_type == cpal::SampleFormat::F32 {
                bestf = Some(f.clone());
                break;
            }
        }

        let format = match bestf {
            Some(f) => f,
            None => return None,
        };

        cpal::Voice::new(&endpoint, &format).ok().map(|v| CpalPlayer { voice: v })
    }
}

impl rboy::AudioPlayer for CpalPlayer {
    fn play(&mut self, buf_left: &[f32], buf_right: &[f32]) {
        debug_assert!(buf_left.len() == buf_right.len());

        let left_idx = self.voice.format().channels.iter().position(|c| *c == cpal::ChannelPosition::FrontLeft);
        let right_idx = self.voice.format().channels.iter().position(|c| *c == cpal::ChannelPosition::FrontRight);

        let channel_count = self.voice.format().channels.len();

        let count = buf_left.len();
        let mut done = 0;
        let mut lastdone = count;

        while lastdone != done && done < count {
            lastdone = done;
            let buf_left_next = &buf_left[done..];
            let buf_right_next = &buf_right[done..];
            match self.voice.append_data(count - done) {
                cpal::UnknownTypeBuffer::U16(mut buffer) => {
                    for (i, sample) in buffer.chunks_mut(channel_count).enumerate() {
                        if sample.len() < channel_count {
                            break;
                        }
                        if let Some(idx) = left_idx {
                            sample[idx] = (buf_left_next[i] * (std::i16::MAX as f32) + (std::i16::MAX as f32)) as u16;
                        }
                        if let Some(idx) = right_idx {
                            sample[idx] = (buf_right_next[i] * (std::i16::MAX as f32) + (std::i16::MAX as f32)) as u16;
                        }
                        done += 1;
                    }
                }
                cpal::UnknownTypeBuffer::I16(mut buffer) => {
                    for (i, sample) in buffer.chunks_mut(channel_count).enumerate() {
                        if sample.len() < channel_count {
                            break;
                        }
                        if let Some(idx) = left_idx {
                            sample[idx] = (buf_left_next[i] * std::i16::MAX as f32) as i16;
                        }
                        if let Some(idx) = right_idx {
                            sample[idx] = (buf_right_next[i] * std::i16::MAX as f32) as i16;
                        }
                        done += 1;
                    }
                }
                cpal::UnknownTypeBuffer::F32(mut buffer) => {
                    for (i, sample) in buffer.chunks_mut(channel_count).enumerate() {
                        if sample.len() < channel_count {
                            break;
                        }
                        if let Some(idx) = left_idx {
                            sample[idx] = buf_left_next[i];
                        }
                        if let Some(idx) = right_idx {
                            sample[idx] = buf_right_next[i];
                        }
                        done += 1;
                    }
                }
            }
        }
        self.voice.play();
    }

    fn samples_rate(&self) -> u32 {
        self.voice.format().samples_rate.0
    }

    fn underflowed(&self) -> bool {
        self.voice.underflowed()
    }
}
<commit_msg>Smarter audio format selection<commit_after>#![crate_name = "rboy"]

extern crate clap;
extern crate cpal;
extern crate glium;
extern crate rboy;

use glium::DisplayBuild;
use rboy::device::Device;
use std::sync::mpsc::{self, Receiver, SyncSender, TryRecvError, TrySendError};
use std::thread;
use std::error::Error;

const EXITCODE_SUCCESS : i32 = 0;
const EXITCODE_CPULOADFAILS : i32 = 2;

#[derive(Default)]
struct RenderOptions {
    pub linear_interpolation: bool,
}

enum GBEvent {
    KeyUp(rboy::KeypadKey),
    KeyDown(rboy::KeypadKey),
    SpeedUp,
    SpeedDown,
}

fn main() {
    let exit_status = real_main();
    if exit_status != EXITCODE_SUCCESS {
        std::process::exit(exit_status);
    }
}

fn real_main() -> i32 {
    let matches = clap::App::new("rboy")
        .version("0.1")
        .author("Mathijs van de Nes")
        .about("A Gameboy Colour emulator written in Rust")
        .arg(clap::Arg::with_name("filename")
             .help("Sets the ROM file to load")
             .required(true))
        .arg(clap::Arg::with_name("serial")
             .help("Prints the data from the serial port to stdout")
             .short("s")
             .long("serial"))
        .arg(clap::Arg::with_name("classic")
             .help("Forces the emulator to run in classic Gameboy mode")
             .short("c")
             .long("classic"))
        .arg(clap::Arg::with_name("scale")
             .help("Sets the scale of the interface. Default: 2")
             .short("x")
             .long("scale")
             .validator(|s|
                 match s.parse::<u32>() {
                     Err(e) => Err(format!("Could not parse scale: {}", e.description())),
                     Ok(s) if s < 1 => Err("Scale must be at least 1".to_owned()),
                     Ok(s) if s > 8 => Err("Scale may be at most 8".to_owned()),
                     Ok(..) => Ok(()),
                 })
             .takes_value(true))
        .arg(clap::Arg::with_name("audio")
             .help("Enables audio")
             .short("a")
             .long("audio"))
        .get_matches();

    let opt_serial = matches.is_present("serial");
    let opt_classic = matches.is_present("classic");
    let opt_audio = matches.is_present("audio");
    let filename = matches.value_of("filename").unwrap();
    let scale = matches.value_of("scale").unwrap_or("2").parse::<u32>().unwrap();

    let cpu = construct_cpu(filename, opt_classic, opt_serial);
    if cpu.is_none() { return EXITCODE_CPULOADFAILS; }
    let mut cpu = cpu.unwrap();
    if opt_audio {
        let player = CpalPlayer::get();
        match player {
            Some(v) => cpu.enable_audio(Box::new(v) as Box<rboy::AudioPlayer>),
            None => { warn("Could not open audio device"); return EXITCODE_CPULOADFAILS; },
        }
    }
    let romname = cpu.romname();

    let (sender1, receiver1) = mpsc::channel();
    let (sender2, receiver2) = mpsc::sync_channel(1);

    let display = glium::glutin::WindowBuilder::new()
        .with_dimensions(rboy::SCREEN_W as u32 * scale, rboy::SCREEN_H as u32 * scale)
        .with_title("RBoy - ".to_owned() + &romname)
        .build_glium()
        .unwrap();

    let mut texture = glium::texture::texture2d::Texture2d::empty_with_format(
            &display,
            glium::texture::UncompressedFloatFormat::U8U8U8,
            glium::texture::MipmapsOption::NoMipmap,
            rboy::SCREEN_W as u32,
            rboy::SCREEN_H as u32)
        .unwrap();

    let mut renderoptions = <RenderOptions as Default>::default();

    let cputhread = thread::spawn(move|| run_cpu(cpu, sender2, receiver1));

    'main : loop {
        for ev in display.poll_events() {
            use glium::glutin::Event;
            use glium::glutin::ElementState::{Pressed, Released};
            use glium::glutin::VirtualKeyCode;

            match ev {
                Event::Closed
                    => break 'main,
                Event::KeyboardInput(Pressed, _, Some(VirtualKeyCode::Escape))
                    => break 'main,
                Event::KeyboardInput(Pressed, _, Some(VirtualKeyCode::Key1))
                    => display.get_window().unwrap().set_inner_size(rboy::SCREEN_W as u32, rboy::SCREEN_H as u32),
                Event::KeyboardInput(Pressed, _, Some(VirtualKeyCode::R))
                    => display.get_window().unwrap().set_inner_size(rboy::SCREEN_W as u32 * scale, rboy::SCREEN_H as u32 * scale),
                Event::KeyboardInput(Pressed, _, Some(VirtualKeyCode::LShift))
                    => { let _ = sender1.send(GBEvent::SpeedUp); },
                Event::KeyboardInput(Released, _, Some(VirtualKeyCode::LShift))
                    => { let _ = sender1.send(GBEvent::SpeedDown); },
                Event::KeyboardInput(Pressed, _, Some(VirtualKeyCode::T))
                    => { renderoptions.linear_interpolation = !renderoptions.linear_interpolation; }
                Event::KeyboardInput(Pressed, _, Some(glutinkey)) => {
                    if let Some(key) = glutin_to_keypad(glutinkey) {
                        let _ = sender1.send(GBEvent::KeyDown(key));
                    }
                },
                Event::KeyboardInput(Released, _, Some(glutinkey)) => {
                    if let Some(key) = glutin_to_keypad(glutinkey) {
                        let _ = sender1.send(GBEvent::KeyUp(key));
                    }
                },
                _ => (),
            }
        }

        match receiver2.try_recv() {
            Ok(data) => recalculate_screen(&display, &mut texture, &*data, &renderoptions),
            Err(TryRecvError::Empty) => (),
            Err(TryRecvError::Disconnected) => break 'main,
        }
    }

    drop(sender1);
    let _ = cputhread.join();

    EXITCODE_SUCCESS
}

fn glutin_to_keypad(key: glium::glutin::VirtualKeyCode) -> Option<rboy::KeypadKey> {
    use glium::glutin::VirtualKeyCode;
    match key {
        VirtualKeyCode::Z => Some(rboy::KeypadKey::A),
        VirtualKeyCode::X => Some(rboy::KeypadKey::B),
        VirtualKeyCode::Up => Some(rboy::KeypadKey::Up),
        VirtualKeyCode::Down => Some(rboy::KeypadKey::Down),
        VirtualKeyCode::Left => Some(rboy::KeypadKey::Left),
        VirtualKeyCode::Right => Some(rboy::KeypadKey::Right),
        VirtualKeyCode::Space => Some(rboy::KeypadKey::Select),
        VirtualKeyCode::Return => Some(rboy::KeypadKey::Start),
        _ => None,
    }
}

fn recalculate_screen(display: &glium::backend::glutin_backend::GlutinFacade,
                      texture: &mut glium::texture::texture2d::Texture2d,
                      datavec: &[u8],
                      renderoptions: &RenderOptions)
{
    use glium::Surface;

    let interpolation_type = if renderoptions.linear_interpolation {
        glium::uniforms::MagnifySamplerFilter::Linear
    }
    else {
        glium::uniforms::MagnifySamplerFilter::Nearest
    };

    let rawimage2d = glium::texture::RawImage2d {
        data: std::borrow::Cow::Borrowed(datavec),
        width: rboy::SCREEN_W as u32,
        height: rboy::SCREEN_H as u32,
        format: glium::texture::ClientFormat::U8U8U8,
    };
    texture.write(
        glium::Rect {
            left: 0,
            bottom: 0,
            width: rboy::SCREEN_W as u32,
            height: rboy::SCREEN_H as u32
        },
        rawimage2d);

    // We use a custom BlitTarget to transform OpenGL coordinates to row-column coordinates
    let target = display.draw();
    let (target_w, target_h) = target.get_dimensions();
    texture.as_surface().blit_whole_color_to(
        &target,
        &glium::BlitTarget {
            left: 0,
            bottom: target_h,
            width: target_w as i32,
            height: -(target_h as i32)
        },
        interpolation_type);
    target.finish().unwrap();
}

fn warn(message: &'static str) {
    use std::io::Write;
    let _ = write!(&mut std::io::stderr(), "{}\n", message);
}

fn construct_cpu(filename: &str, classic_mode: bool, output_serial: bool) -> Option<Device> {
    let opt_c = match classic_mode {
        true => Device::new(filename),
        false => Device::new_cgb(filename),
    };
    let mut c = match opt_c
    {
        Ok(cpu) => { cpu },
        Err(message) => { warn(message); return None; },
    };
    c.set_stdout(output_serial);
    Some(c)
}

fn run_cpu(mut cpu: Device, sender: SyncSender<Vec<u8>>, receiver: Receiver<GBEvent>) {
    let periodic = timer_periodic(16);
    let mut limit_speed = true;

    let waitticks = (4194304f64 / 1000.0 * 16.0).round() as u32;
    let mut ticks = 0;

    'outer: loop {
        while ticks < waitticks {
            ticks += cpu.do_cycle();
            if cpu.check_and_reset_gpu_updated() {
                let data = cpu.get_gpu_data().to_vec();
                if let Err(TrySendError::Disconnected(..)) = sender.try_send(data) {
                    break 'outer;
                }
            }
        }

        ticks -= waitticks;

        'recv: loop {
            match receiver.try_recv() {
                Ok(event) => {
                    match event {
                        GBEvent::KeyUp(key) => cpu.keyup(key),
                        GBEvent::KeyDown(key) => cpu.keydown(key),
                        GBEvent::SpeedUp => limit_speed = false,
                        GBEvent::SpeedDown => { limit_speed = true; cpu.sync_audio(); }
                    }
                },
                Err(TryRecvError::Empty) => break 'recv,
                Err(TryRecvError::Disconnected) => break 'outer,
            }
        }

        if limit_speed { let _ = periodic.recv(); }
    }
}

fn timer_periodic(ms: u64) -> Receiver<()> {
    let (tx, rx) = std::sync::mpsc::sync_channel(1);
    std::thread::spawn(move || {
        loop {
            std::thread::sleep(std::time::Duration::from_millis(ms));
            if tx.send(()).is_err() {
                break;
            }
        }
    });
    rx
}

struct CpalPlayer {
    voice: cpal::Voice,
}

impl CpalPlayer {
    fn get() -> Option<CpalPlayer> {
        if cpal::get_endpoints_list().count() == 0 { return None; }

        let endpoint = match cpal::get_default_endpoint() {
            Some(e) => e,
            None => return None,
        };

        let mut wanted_samplerate = None;
        let mut wanted_sampleformat = None;
        for f in endpoint.get_supported_formats_list().unwrap() {
            match wanted_samplerate {
                None => wanted_samplerate = Some(f.samples_rate),
                Some(cpal::SamplesRate(r)) if r < f.samples_rate.0 && r < 192000 => wanted_samplerate = Some(f.samples_rate),
                _ => {},
            }
            match wanted_sampleformat {
                None => wanted_sampleformat = Some(f.data_type),
                Some(cpal::SampleFormat::F32) => {},
                Some(_) if f.data_type == cpal::SampleFormat::F32 => wanted_sampleformat = Some(f.data_type),
                _ => {},
            }
        }

        if wanted_samplerate.is_none() || wanted_sampleformat.is_none() {
            return None;
        }

        let format = cpal::Format {
            channels: vec![cpal::ChannelPosition::FrontLeft, cpal::ChannelPosition::FrontRight],
            samples_rate: wanted_samplerate.unwrap(),
            data_type: wanted_sampleformat.unwrap(),
        };

        match cpal::Voice::new(&endpoint, &format) {
            Ok(v) => Some(CpalPlayer { voice: v }),
            Err(_) => None,
        }
    }
}

impl rboy::AudioPlayer for CpalPlayer {
    fn play(&mut self, buf_left: &[f32], buf_right: &[f32]) {
        debug_assert!(buf_left.len() == buf_right.len());

        let left_idx = self.voice.format().channels.iter().position(|c| *c == cpal::ChannelPosition::FrontLeft);
        let right_idx = self.voice.format().channels.iter().position(|c| *c == cpal::ChannelPosition::FrontRight);

        let channel_count = self.voice.format().channels.len();

        let count = buf_left.len();
        let mut done = 0;
        let mut lastdone = count;

        while lastdone != done && done < count {
            lastdone = done;
            let buf_left_next = &buf_left[done..];
            let buf_right_next = &buf_right[done..];
            match self.voice.append_data(count - done) {
                cpal::UnknownTypeBuffer::U16(mut buffer) => {
                    for (i, sample) in buffer.chunks_mut(channel_count).enumerate() {
                        if sample.len() < channel_count {
                            break;
                        }
                        if let Some(idx) = left_idx {
                            sample[idx] = (buf_left_next[i] * (std::i16::MAX as f32) + (std::i16::MAX as f32)) as u16;
                        }
                        if let Some(idx) = right_idx {
                            sample[idx] = (buf_right_next[i] * (std::i16::MAX as f32) + (std::i16::MAX as f32)) as u16;
                        }
                        done += 1;
                    }
                }
                cpal::UnknownTypeBuffer::I16(mut buffer) => {
                    for (i, sample) in buffer.chunks_mut(channel_count).enumerate() {
                        if sample.len() < channel_count {
                            break;
                        }
                        if let Some(idx) = left_idx {
                            sample[idx] = (buf_left_next[i] * std::i16::MAX as f32) as i16;
                        }
                        if let Some(idx) = right_idx {
                            sample[idx] = (buf_right_next[i] * std::i16::MAX as f32) as i16;
                        }
                        done += 1;
                    }
                }
                cpal::UnknownTypeBuffer::F32(mut buffer) => {
                    for (i, sample) in buffer.chunks_mut(channel_count).enumerate() {
                        if sample.len() < channel_count {
                            break;
                        }
                        if let Some(idx) = left_idx {
                            sample[idx] = buf_left_next[i];
                        }
                        if let Some(idx) = right_idx {
                            sample[idx] = buf_right_next[i];
                        }
                        done += 1;
                    }
                }
            }
        }
        self.voice.play();
    }

    fn samples_rate(&self) -> u32 {
        self.voice.format().samples_rate.0
    }

    fn underflowed(&self) -> bool {
        self.voice.underflowed()
    }
}
<|endoftext|>


<commit_before>// TODO: many items from tokio-core::io have been deprecated in favour of tokio-io
#![allow(deprecated)]

#[macro_use] extern crate log;
#[cfg(debug_assertions)]
extern crate env_logger;
extern crate futures;
extern crate getopts;
extern crate librespot;
extern crate tokio_core;
extern crate tokio_signal;

#[cfg(debug_assertions)]
use env_logger::LogBuilder;
use futures::{Future, Async, Poll, Stream};
#[cfg(debug_assertions)]
use std::env;
use std::io::{self, stderr, Write};
use std::path::PathBuf;
use std::process::exit;
use std::str::FromStr;
use tokio_core::reactor::{Handle, Core};
use tokio_core::io::IoStream;
use std::mem;

use librespot::spirc::{Spirc, SpircTask};
use librespot::authentication::{get_credentials, Credentials};
#[cfg(not(target_os="windows"))]
use librespot::authentication::discovery::{discovery, DiscoveryStream};
use librespot::audio_backend;
use librespot::cache::Cache;
use librespot::player::Player;
use librespot::session::{Bitrate, Config, Session};
use librespot::mixer::{self, Mixer};
use librespot::util::SpotifyId;

use librespot::version;

fn usage(program: &str, opts: &getopts::Options) -> String {
    let brief = format!("Usage: {} [options]", program);
    opts.usage(&brief)
}

#[cfg(debug_assertions)]
fn setup_logging(verbose: bool) {
    let mut builder = LogBuilder::new();
    match env::var("RUST_LOG") {
        Ok(config) => {
            builder.parse(&config);
            builder.init().unwrap();

            if verbose {
                warn!("`--verbose` flag overidden by `RUST_LOG` environment variable");
            }
        }
        Err(_) => {
            if verbose {
                builder.parse("mdns=info,librespot=trace");
            } else {
                builder.parse("mdns=error,librespot=warn");
            }
            builder.init().unwrap();
        }
    }
}

#[derive(Clone)]
struct Setup {
    mixer: fn() -> Box<Mixer>,

	name: String,
    cache: Option<Cache>,
    config: Config,
    credentials: Option<Credentials>,
    authenticate: bool,
    enable_discovery: bool,

    single_track: Option<String>,
    start_position: u32,
}

fn setup(args: &[String]) -> Setup {
    let mut opts = getopts::Options::new();
    opts.optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE")
        .reqopt("n", "name", "Device name", "NAME")
        .optopt("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE")
        .optopt("", "onstart", "Run PROGRAM when playback is about to begin.", "PROGRAM")
        .optopt("", "onstop", "Run PROGRAM when playback has ended.", "PROGRAM")
        .optopt("", "single-track", "Play a single track ID and exit.", "ID")
        .optopt("", "start-position", "Position (in ms) where playback should be started. Only valid with the --single-track option.", "STARTPOSITION")
        .optopt("u", "username", "Username to sign in with", "USERNAME")
        .optopt("p", "password", "Password", "PASSWORD")
        .optflag("a", "authenticate", "Authenticate given username and password. Make sure you define a cache folder to store credentials.")
        .optflag("", "disable-discovery", "Disable discovery mode")
        .optflag("x", "check", "Run quick internal check")
        .optopt("", "mixer", "Mixer to use", "MIXER");

	#[cfg(debug_assertions)]
    opts.optflag("v", "verbose", "Enable verbose output");

    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(f) => {
            writeln!(stderr(), "error: {}\n{}", f.to_string(), usage(&args[0], &opts)).unwrap();
            exit(1);
        }
    };

    if matches.opt_present("check") {
    	println!("ok");
    	exit(1);
    }

	#[cfg(debug_assertions)]
	{
	    let verbose = matches.opt_present("verbose");
	    setup_logging(verbose);
	}

    info!("librespot {} ({}). Built on {}.",
             version::short_sha(),
             version::commit_date(),
             version::short_now());

    let mixer_name = matches.opt_str("mixer");
    let mixer = mixer::find(mixer_name.as_ref())
        .expect("Invalid mixer");

    let bitrate = matches.opt_str("b").as_ref()
        .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
        .unwrap_or(Bitrate::Bitrate160);

    let name = matches.opt_str("name").unwrap();
    let device_id = librespot::session::device_id(&name);

    let cache = matches.opt_str("c").map(|cache_location| {
        Cache::new(PathBuf::from(cache_location))
    });

    let cached_credentials = cache.as_ref().and_then(Cache::credentials);

    let credentials = get_credentials(matches.opt_str("username"),
                                      matches.opt_str("password"),
                                      cached_credentials);

    let authenticate = matches.opt_present("authenticate");

#[cfg(not(target_os="windows"))]
    let enable_discovery = !matches.opt_present("disable-discovery");
#[cfg(target_os="windows")]
    let enable_discovery = false;

    let start_position = matches.opt_str("start-position")
    	.unwrap_or("0".to_string())
    	.parse().unwrap_or(0.0);

    let config = Config {
        user_agent: version::version_string(),
        device_id: device_id,
        bitrate: bitrate,
        onstart: matches.opt_str("onstart"),
        onstop: matches.opt_str("onstop"),
    };

    Setup {
		name: name,
        cache: cache,
        config: config,
        credentials: credentials,
        authenticate: authenticate,
        enable_discovery: enable_discovery,
        mixer: mixer,

        single_track: matches.opt_str("single-track"),
        start_position: (start_position * 1000.0) as u32,
    }
}

struct Main {
	name: String,
    cache: Option<Cache>,
    config: Config,
    mixer: fn() -> Box<Mixer>,
    handle: Handle,

#[cfg(not(target_os="windows"))]
    discovery: Option<DiscoveryStream>,
#[cfg(target_os="windows")]
    discovery: Option<String>,
    signal: IoStream<()>,

    spirc: Option<Spirc>,
    spirc_task: Option<SpircTask>,
    connect: Box<Future<Item=Session, Error=io::Error>>,

    player: Option<Player>,

    shutdown: bool,
    authenticate: bool,
}

impl Main {
    fn new(handle: Handle,
           name: String,
           config: Config,
           cache: Option<Cache>,
           mixer: fn() -> Box<Mixer>,
           authenticate: bool,
    ) -> Main
    {
        Main {
            handle: handle.clone(),
            name: name,
            cache: cache,
            config: config,
            mixer: mixer,

            connect: Box::new(futures::future::empty()),
            discovery: None,
            spirc: None,
            spirc_task: None,

            player: None,

            shutdown: false,
            authenticate: authenticate,
            signal: tokio_signal::ctrl_c(&handle).flatten_stream().boxed(),
        }
    }

#[cfg(not(target_os="windows"))]
    fn discovery(&mut self) {
        let device_id = self.config.device_id.clone();
        let name = self.name.clone();

        self.discovery = Some(discovery(&self.handle, name, device_id).unwrap());
    }

    fn credentials(&mut self, credentials: Credentials) {
        let config = self.config.clone();
        let handle = self.handle.clone();

        let connection = Session::connect(config, credentials, self.cache.clone(), handle);

        self.connect = connection;
        self.spirc = None;
        let task = mem::replace(&mut self.spirc_task, None);
        if let Some(task) = task {
            self.handle.spawn(task);
        }
    }
}

impl Future for Main {
    type Item = ();
    type Error = ();

    fn poll(&mut self) -> Poll<(), ()> {
        loop {
            let mut progress = false;

#[cfg(not(target_os="windows"))] {
            if let Some(Async::Ready(Some(creds))) = self.discovery.as_mut().map(|d| d.poll().unwrap()) {
                if let Some(ref spirc) = self.spirc {
                    spirc.shutdown();
                }
                self.credentials(creds);

                progress = true;
            }
}

            if let Async::Ready(session) = self.connect.poll().unwrap() {
            	if self.authenticate {
					if !self.shutdown {
						if let Some(ref spirc) = self.spirc {
							spirc.shutdown();
						}
						self.shutdown = true;
					} else {
						return Ok(Async::Ready(()));
					}
            	}
            	else {
					self.connect = Box::new(futures::future::empty());
					let mixer = (self.mixer)();

					let audio_filter = mixer.get_audio_filter();
					let backend = audio_backend::find(None).unwrap();
					let player = Player::new(session.clone(), audio_filter, move || {
						(backend)(None)
					});

					self.player = Some(player.clone());

					let (spirc, spirc_task) = Spirc::new(self.name.clone(), session, player, mixer);
					self.spirc = Some(spirc);
					self.spirc_task = Some(spirc_task);
				}

                progress = true;
            }

            if let Async::Ready(Some(())) = self.signal.poll().unwrap() {
                if !self.shutdown {
                    if let Some(ref spirc) = self.spirc {
                        spirc.shutdown();
                    }
                    self.shutdown = true;
                } else {
                    return Ok(Async::Ready(()));
                }

                progress = true;
            }

            if let Some(ref mut spirc_task) = self.spirc_task {
                if let Async::Ready(()) = spirc_task.poll().unwrap() {
                    if self.shutdown {
                        return Ok(Async::Ready(()));
                    } else {
                        panic!("Spirc shut down unexpectedly");
                    }
                }
            }

            if !progress {
                return Ok(Async::NotReady);
            }
        }
    }
}

fn main() {
    let mut core = Core::new().unwrap();
    let handle = core.handle();

    let args: Vec<String> = std::env::args().collect();
    let Setup { name, config, cache, enable_discovery, credentials, authenticate, mixer, single_track, start_position } = setup(&args);

	if let Some(ref track_id) = single_track {
		match credentials {
			Some(credentials) => {
				let backend = audio_backend::find(None).unwrap();

				let track = SpotifyId::from_base62(
									track_id.replace("spotty://", "")
									.replace("spotify://", "")
									.replace("track:", "")
									.as_str());

				let session = core.run(Session::connect(config, credentials, None, handle)).unwrap();

				let player = Player::new(session.clone(), None, move || (backend)(None));

				core.run(player.load(track, true, start_position)).unwrap();
			}
			None => {
				println!("Missing credentials");
			}
	    }
	}
	else if authenticate && !enable_discovery {
        core.run(Session::connect(config, credentials.unwrap(), cache.clone(), handle)).unwrap();
		println!("authorized");
	}
	else {
		let mut task = Main::new(handle, name, config, cache, mixer, authenticate);
		if enable_discovery {
#[cfg(not(target_os="windows"))]
			task.discovery();
		}
		if let Some(credentials) = credentials {
			task.credentials(credentials);
		}

		core.run(task).unwrap()
    }
}

<commit_msg>Report helper app's version, not librespot's<commit_after>// TODO: many items from tokio-core::io have been deprecated in favour of tokio-io
#![allow(deprecated)]

#[cfg(debug_assertions)]
#[macro_use] extern crate log;
#[cfg(debug_assertions)]
extern crate env_logger;
extern crate futures;
extern crate getopts;
extern crate librespot;
extern crate tokio_core;
extern crate tokio_signal;

#[cfg(debug_assertions)]
use env_logger::LogBuilder;
use futures::{Future, Async, Poll, Stream};
#[cfg(debug_assertions)]
use std::env;
use std::io::{self, stderr, Write};
use std::path::PathBuf;
use std::process::exit;
use std::str::FromStr;
use tokio_core::reactor::{Handle, Core};
use tokio_core::io::IoStream;
use std::mem;

use librespot::spirc::{Spirc, SpircTask};
use librespot::authentication::{get_credentials, Credentials};
#[cfg(not(target_os="windows"))]
use librespot::authentication::discovery::{discovery, DiscoveryStream};
use librespot::audio_backend;
use librespot::cache::Cache;
use librespot::player::Player;
use librespot::session::{Bitrate, Config, Session};
use librespot::mixer::{self, Mixer};
use librespot::util::SpotifyId;

const VERSION: &'static str = concat!(env!("CARGO_PKG_NAME"), " v", env!("CARGO_PKG_VERSION"));

fn usage(program: &str, opts: &getopts::Options) -> String {
   	println!("{}", VERSION.to_string());

    let brief = format!("Usage: {} [options]", program);
    opts.usage(&brief)
}

#[cfg(debug_assertions)]
fn setup_logging(verbose: bool) {
    let mut builder = LogBuilder::new();
    match env::var("RUST_LOG") {
        Ok(config) => {
            builder.parse(&config);
            builder.init().unwrap();

            if verbose {
                warn!("`--verbose` flag overidden by `RUST_LOG` environment variable");
            }
        }
        Err(_) => {
            if verbose {
                builder.parse("mdns=info,librespot=trace");
            } else {
                builder.parse("mdns=error,librespot=warn");
            }
            builder.init().unwrap();
        }
    }
}

#[derive(Clone)]
struct Setup {
    mixer: fn() -> Box<Mixer>,

	name: String,
    cache: Option<Cache>,
    config: Config,
    credentials: Option<Credentials>,
    authenticate: bool,
    enable_discovery: bool,

    single_track: Option<String>,
    start_position: u32,
}

fn setup(args: &[String]) -> Setup {
    let mut opts = getopts::Options::new();
    opts.optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE")
        .reqopt("n", "name", "Device name", "NAME")
        .optopt("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE")
        .optopt("", "onstart", "Run PROGRAM when playback is about to begin.", "PROGRAM")
        .optopt("", "onstop", "Run PROGRAM when playback has ended.", "PROGRAM")
        .optopt("", "single-track", "Play a single track ID and exit.", "ID")
        .optopt("", "start-position", "Position (in ms) where playback should be started. Only valid with the --single-track option.", "STARTPOSITION")
        .optopt("u", "username", "Username to sign in with", "USERNAME")
        .optopt("p", "password", "Password", "PASSWORD")
        .optflag("a", "authenticate", "Authenticate given username and password. Make sure you define a cache folder to store credentials.")
        .optflag("", "disable-discovery", "Disable discovery mode")
        .optflag("x", "check", "Run quick internal check")
        .optopt("", "mixer", "Mixer to use", "MIXER");

	#[cfg(debug_assertions)]
    opts.optflag("v", "verbose", "Enable verbose output");

    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(f) => {
            writeln!(stderr(), "error: {}\n{}", f.to_string(), usage(&args[0], &opts)).unwrap();
            exit(1);
        }
    };

    if matches.opt_present("check") {
    	println!("ok {}", VERSION.to_string());
    	exit(1);
    }

	#[cfg(debug_assertions)]
	{
	    let verbose = matches.opt_present("verbose");
	    setup_logging(verbose);
	}

    let mixer_name = matches.opt_str("mixer");
    let mixer = mixer::find(mixer_name.as_ref())
        .expect("Invalid mixer");

    let bitrate = matches.opt_str("b").as_ref()
        .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
        .unwrap_or(Bitrate::Bitrate160);

    let name = matches.opt_str("name").unwrap();
    let device_id = librespot::session::device_id(&name);

    let cache = matches.opt_str("c").map(|cache_location| {
        Cache::new(PathBuf::from(cache_location))
    });

    let cached_credentials = cache.as_ref().and_then(Cache::credentials);

    let credentials = get_credentials(matches.opt_str("username"),
                                      matches.opt_str("password"),
                                      cached_credentials);

    let authenticate = matches.opt_present("authenticate");

#[cfg(not(target_os="windows"))]
    let enable_discovery = !matches.opt_present("disable-discovery");
#[cfg(target_os="windows")]
    let enable_discovery = false;

    let start_position = matches.opt_str("start-position")
    	.unwrap_or("0".to_string())
    	.parse().unwrap_or(0.0);

    let config = Config {
        user_agent: VERSION.to_string(),
        device_id: device_id,
        bitrate: bitrate,
        onstart: matches.opt_str("onstart"),
        onstop: matches.opt_str("onstop"),
    };

    Setup {
		name: name,
        cache: cache,
        config: config,
        credentials: credentials,
        authenticate: authenticate,
        enable_discovery: enable_discovery,
        mixer: mixer,

        single_track: matches.opt_str("single-track"),
        start_position: (start_position * 1000.0) as u32,
    }
}

struct Main {
	name: String,
    cache: Option<Cache>,
    config: Config,
    mixer: fn() -> Box<Mixer>,
    handle: Handle,

#[cfg(not(target_os="windows"))]
    discovery: Option<DiscoveryStream>,
#[cfg(target_os="windows")]
    discovery: Option<String>,
    signal: IoStream<()>,

    spirc: Option<Spirc>,
    spirc_task: Option<SpircTask>,
    connect: Box<Future<Item=Session, Error=io::Error>>,

    player: Option<Player>,

    shutdown: bool,
    authenticate: bool,
}

impl Main {
    fn new(handle: Handle,
           name: String,
           config: Config,
           cache: Option<Cache>,
           mixer: fn() -> Box<Mixer>,
           authenticate: bool,
    ) -> Main
    {
        Main {
            handle: handle.clone(),
            name: name,
            cache: cache,
            config: config,
            mixer: mixer,

            connect: Box::new(futures::future::empty()),
            discovery: None,
            spirc: None,
            spirc_task: None,

            player: None,

            shutdown: false,
            authenticate: authenticate,
            signal: tokio_signal::ctrl_c(&handle).flatten_stream().boxed(),
        }
    }

#[cfg(not(target_os="windows"))]
    fn discovery(&mut self) {
        let device_id = self.config.device_id.clone();
        let name = self.name.clone();

        self.discovery = Some(discovery(&self.handle, name, device_id).unwrap());
    }

    fn credentials(&mut self, credentials: Credentials) {
        let config = self.config.clone();
        let handle = self.handle.clone();

        let connection = Session::connect(config, credentials, self.cache.clone(), handle);

        self.connect = connection;
        self.spirc = None;
        let task = mem::replace(&mut self.spirc_task, None);
        if let Some(task) = task {
            self.handle.spawn(task);
        }
    }
}

impl Future for Main {
    type Item = ();
    type Error = ();

    fn poll(&mut self) -> Poll<(), ()> {
        loop {
            let mut progress = false;

#[cfg(not(target_os="windows"))] {
            if let Some(Async::Ready(Some(creds))) = self.discovery.as_mut().map(|d| d.poll().unwrap()) {
                if let Some(ref spirc) = self.spirc {
                    spirc.shutdown();
                }
                self.credentials(creds);

                progress = true;
            }
}

            if let Async::Ready(session) = self.connect.poll().unwrap() {
            	if self.authenticate {
					if !self.shutdown {
						if let Some(ref spirc) = self.spirc {
							spirc.shutdown();
						}
						self.shutdown = true;
					} else {
						return Ok(Async::Ready(()));
					}
            	}
            	else {
					self.connect = Box::new(futures::future::empty());
					let mixer = (self.mixer)();

					let audio_filter = mixer.get_audio_filter();
					let backend = audio_backend::find(None).unwrap();
					let player = Player::new(session.clone(), audio_filter, move || {
						(backend)(None)
					});

					self.player = Some(player.clone());

					let (spirc, spirc_task) = Spirc::new(self.name.clone(), session, player, mixer);
					self.spirc = Some(spirc);
					self.spirc_task = Some(spirc_task);
				}

                progress = true;
            }

            if let Async::Ready(Some(())) = self.signal.poll().unwrap() {
                if !self.shutdown {
                    if let Some(ref spirc) = self.spirc {
                        spirc.shutdown();
                    }
                    self.shutdown = true;
                } else {
                    return Ok(Async::Ready(()));
                }

                progress = true;
            }

            if let Some(ref mut spirc_task) = self.spirc_task {
                if let Async::Ready(()) = spirc_task.poll().unwrap() {
                    if self.shutdown {
                        return Ok(Async::Ready(()));
                    } else {
                        panic!("Spirc shut down unexpectedly");
                    }
                }
            }

            if !progress {
                return Ok(Async::NotReady);
            }
        }
    }
}

fn main() {
    let mut core = Core::new().unwrap();
    let handle = core.handle();

    let args: Vec<String> = std::env::args().collect();
    let Setup { name, config, cache, enable_discovery, credentials, authenticate, mixer, single_track, start_position } = setup(&args);

	if let Some(ref track_id) = single_track {
		match credentials {
			Some(credentials) => {
				let backend = audio_backend::find(None).unwrap();

				let track = SpotifyId::from_base62(
									track_id.replace("spotty://", "")
									.replace("spotify://", "")
									.replace("track:", "")
									.as_str());

				let session = core.run(Session::connect(config, credentials, None, handle)).unwrap();

				let player = Player::new(session.clone(), None, move || (backend)(None));

				core.run(player.load(track, true, start_position)).unwrap();
			}
			None => {
				println!("Missing credentials");
			}
	    }
	}
	else if authenticate && !enable_discovery {
        core.run(Session::connect(config, credentials.unwrap(), cache.clone(), handle)).unwrap();
		println!("authorized");
	}
	else {
		let mut task = Main::new(handle, name, config, cache, mixer, authenticate);
		if enable_discovery {
#[cfg(not(target_os="windows"))]
			task.discovery();
		}
		if let Some(credentials) = credentials {
			task.credentials(credentials);
		}

		core.run(task).unwrap()
    }
}

<|endoftext|>


<commit_before>extern crate feroxide;
use feroxide::*;
use feroxide::data_atoms::*;

extern crate gtk;
use gtk::prelude::*;
use gtk::*;

use std::rc::Rc;
use std::cell::RefCell;

macro_rules! clone {
    (@param _) => ( _ );
    (@param $x:ident) => ( $x );
    ($($n:ident),+ => move || $body:expr) => (
        {
            $( let $n = $n.clone(); )+
            move || $body
        }
    );
    ($($n:ident),+ => move |$($p:tt),+| $body:expr) => (
        {
            $( let $n = $n.clone(); )+
            move |$(clone!(@param $p),)+| $body
        }
    );
}

fn main() {
    #![allow(unused)]

    println!("Hello World!");

    // Testing feroxide
    println!("Water has {} g/mol", Molecule::from_string("H2O".to_owned()).unwrap().mass());

    // Create container
    let mut fo_container = Rc::new(RefCell::new(feroxide::Container::<Molecule> {
        contents: vec! {},
        available_energy: 0.0
    }));

    // Create window
    if gtk::init().is_err() {
        panic!("Failed to initialise GTK.");
    }

    let window = Window::new(WindowType::Toplevel);
    window.set_title("feroxide");
    window.set_position(WindowPosition::Center);
    window.set_size_request(400, 400);

    window.connect_delete_event(|_, _| {
        gtk::main_quit();
        Inhibit(false)
    });

    let menu_bar = MenuBar::new();

    let menu_add = Menu::new();
    let item_add = MenuItem::new_with_label("Add");
    let item_hydrogen = MenuItem::new_with_label("H: hydrogen");
    let item_oxygen = MenuItem::new_with_label("O: oxygen");

    menu_add.append(& item_hydrogen);
    menu_add.append(& item_oxygen);
    item_add.set_submenu(Some(& menu_add));

    menu_bar.append(& item_add);

    /*
    let item = MenuItem::new_with_label("Add one");

    #[derive(Clone)]
    struct Variable {
        pub value: u32
    }

    let mut x = Rc::new(RefCell::new(Variable {
        value: 0
    }));

    impl Variable {
        pub fn add(&mut self, x: u32) {
            self.value += x;
        }
    }

    item.connect_activate(clone!(x => move |_| {
        x.borrow_mut().add(10);
    }));
    */

    item_hydrogen.connect_activate(clone!(fo_container => move |_| {
        let mut fo_container = fo_container.borrow_mut();

        fo_container.add_elements(&vec! {
            ContainerCompound {
                element: molecule_from_atom!(HYDROGEN),
                moles: 10.0
            }
        });

        println!("{}", fo_container.stringify());
    }));

    window.add(& menu_bar);

    window.show_all();

    gtk::main();
}
<commit_msg>Add Oxygen button<commit_after>extern crate feroxide;
use feroxide::*;
use feroxide::data_atoms::*;

extern crate gtk;
use gtk::prelude::*;
use gtk::*;

use std::rc::Rc;
use std::cell::RefCell;

macro_rules! clone {
    (@param _) => ( _ );
    (@param $x:ident) => ( $x );
    ($($n:ident),+ => move || $body:expr) => (
        {
            $( let $n = $n.clone(); )+
            move || $body
        }
    );
    ($($n:ident),+ => move |$($p:tt),+| $body:expr) => (
        {
            $( let $n = $n.clone(); )+
            move |$(clone!(@param $p),)+| $body
        }
    );
}

fn main() {
    #![allow(unused)]

    println!("Hello World!");

    // Testing feroxide
    println!("Water has {} g/mol", Molecule::from_string("H2O".to_owned()).unwrap().mass());

    // Create container
    let mut fo_container = Rc::new(RefCell::new(feroxide::Container::<Molecule> {
        contents: vec! {},
        available_energy: 0.0
    }));

    // Create window
    if gtk::init().is_err() {
        panic!("Failed to initialise GTK.");
    }

    let window = Window::new(WindowType::Toplevel);
    window.set_title("feroxide");
    window.set_position(WindowPosition::Center);
    window.set_size_request(400, 400);

    window.connect_delete_event(|_, _| {
        gtk::main_quit();
        Inhibit(false)
    });

    let menu_bar = MenuBar::new();

    let menu_add = Menu::new();
    let item_add = MenuItem::new_with_label("Add");
    let item_hydrogen = MenuItem::new_with_label("H: hydrogen");
    let item_oxygen = MenuItem::new_with_label("O: oxygen");

    menu_add.append(& item_hydrogen);
    menu_add.append(& item_oxygen);
    item_add.set_submenu(Some(& menu_add));

    menu_bar.append(& item_add);

    item_hydrogen.connect_activate(clone!(fo_container => move |_| {
        let mut fo_container = fo_container.borrow_mut();

        fo_container.add_elements(&vec! {
            ContainerCompound {
                element: molecule_from_atom!(HYDROGEN),
                moles: 10.0
            }
        });

        println!("{}", fo_container.stringify());
    }));

    item_oxygen.connect_activate(clone!(fo_container => move |_| {
        let mut fo_container = fo_container.borrow_mut();

        fo_container.add_elements(&vec! {
            ContainerCompound {
                element: molecule_from_atom!(OXYGEN),
                moles: 10.0
            }
        });

        println!("{}", fo_container.stringify());
    }));

    window.add(& menu_bar);

    window.show_all();

    gtk::main();
}
<|endoftext|>


<commit_before>extern crate clap;
extern crate unlimited;

//
use std::sync::mpsc::channel;
use std::thread;

use clap::{App, Arg};

use unlimited::core;
use unlimited::core::config::Config;

use unlimited::ui;

const VERSION: &'static str = env!("CARGO_PKG_VERSION");

fn main() {
    let config = parse_command_line();

    let (ui_tx, ui_rx) = channel();
    let (core_tx, core_rx) = channel();

    let ui_name = config.ui_frontend.clone();

    let core_th = { Some(thread::spawn(move || core::start(config, core_rx, ui_tx))) };

    ui::main_loop(ui_name.as_ref(), ui_rx, core_tx);

    if let Some(core_handle) = core_th {
        core_handle.join().unwrap()
    }
}

fn parse_command_line() -> Config {
    let matches = App::new("unlimited")
        .version(VERSION)
        .author("Carl-Erwin Griffith <carl.erwin@gmail.com>")
        .about("unlimited is an experimental editor")
        .args_from_usage("--ui, --ui=[termion|ncurses] 'select user interface frontend'")
        .arg(
            Arg::with_name("FILES")
                .help("list of the files to open")
                .required(false)
                .multiple(true),
        )
        .get_matches();

    let mut ui_frontend = String::new();
    if matches.is_present("ui") {
        let v: Vec<&str> = matches.values_of("ui").unwrap().collect();
        ui_frontend = v[0].to_owned();
    }

    let mut files_list = Vec::new();
    if matches.is_present("FILES") {
        let strs: Vec<&str> = matches.values_of("FILES").unwrap().collect();
        files_list = strs.into_iter().map(|x| x.to_owned()).collect()
    }

    Config {
        files_list,
        ui_frontend,
    }
}
<commit_msg>simplifications<commit_after>extern crate clap;
extern crate unlimited;

//
use std::sync::mpsc::channel;
use std::thread;

use clap::{App, Arg};

use unlimited::core;
use unlimited::core::config::Config;

use unlimited::ui;

const VERSION: &'static str = env!("CARGO_PKG_VERSION");

fn main() {
    let config = parse_command_line();

    let (ui_tx, ui_rx) = channel();
    let (core_tx, core_rx) = channel();

    let ui_name = config.ui_frontend.clone();

    let core_th = { Some(thread::spawn(move || core::start(config, core_rx, ui_tx))) };

    ui::main_loop(ui_name.as_ref(), ui_rx, core_tx);

    if let Some(core_handle) = core_th {
        core_handle.join().unwrap()
    }
}

fn parse_command_line() -> Config {
    let matches = App::new("unlimited")
        .version(VERSION)
        .author("Carl-Erwin Griffith <carl.erwin@gmail.com>")
        .about("unlimited is an experimental editor")
        .args_from_usage("--ui, --ui=[termion|ncurses] 'select user interface frontend'")
        .arg(
            Arg::with_name("FILES")
                .help("list of the files to open")
                .required(false)
                .multiple(true),
        )
        .get_matches();

    let mut ui_frontend = String::new();
    if matches.is_present("ui") {
        ui_frontend = matches.values_of("ui").unwrap().collect::<String>();
    }

    let mut files_list = Vec::new();
    if matches.is_present("FILES") {
        files_list = matches
            .values_of("FILES")
            .unwrap()
            .map(|x| x.to_owned())
            .collect();
    }

    Config {
        files_list,
        ui_frontend,
    }
}
<|endoftext|>