<commit_before>/*
Copyright 2017 Aron Heinecke

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

extern crate reqwest;
extern crate json;
extern crate flate2;
#[macro_use]
extern crate log;
extern crate log4rs;
#[macro_use]
extern crate quick_error;
extern crate mysql;
extern crate regex;
extern crate chrono;
extern crate toml;
extern crate timer;
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate clap;
extern crate sendmail;

mod http;
mod error;
mod parser;
mod db;
mod config;

use http::HeaderType;

use std::fs::{File,metadata};
use std::env::current_exe;
use std::fs::DirBuilder;
use std::io::Write;

use std::sync::Arc;

use sendmail::email;

use chrono::naive::{NaiveTime,NaiveDateTime};
use chrono::offset::Local;
use chrono::Timelike;

use mysql::Pool;

use error::Error;

use config::Config;

use clap::{Arg,App,SubCommand};

const USER_AGENT: &'static str = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0";
const REFERER: &'static str = "http://crossfire.z8games.com/";
const CONFIG_PATH: &'static str = "config/config.toml";
const LOG_PATH: &'static str = "config/log.yml";
const INTERVALL_H: i64 = 24; // execute intervall

fn main() {
    match init_log() {
        Err(e) => println!("Error on config initialization: {}",e),
        Ok(_) => println!("Initialized log")
    }
    info!("Clan tools crawler v0.2.0");

    let config = Arc::new(config::init_config());
    let pool = match db::new(config.db.ip.clone(),
        config.db.port,
        config.db.user.clone(),
        config.db.password.clone(),
        config.db.db.clone()) {
        Err(e) => {error!("Can't connect to DB! {}",e); panic!();},
        Ok(v) => Arc::new(v)
    };
    let timer = timer::Timer::new();

    let app = App::new("Clantool")
                    .version("2.0")
                    .author("Aron H. <aron.heinecke@t-online.de>")
                    .about("Gathers statistics about CF-NA clans. Starts a daemon per default")
                    .subcommand(SubCommand::with_name("fcrawl")
                        .about("force run crawl & exit"))
                    .subcommand(SubCommand::with_name("mail-test")
                        .about("Test mail sending")
                        /*.arg(Arg::with_name("mail")
                            .help("Required mail address")
                            .takes_value(true)
                            .required(true))
                        */    )
                    .subcommand(SubCommand::with_name("checkdb")
                        .about("checks DB for missing entries or doubles and corrects those")
                        .arg(Arg::with_name("simulate")
                            .short("s")
                            .help("simulation mode, leaves DB unchanged")))
                    .get_matches();
    trace!("{:?}",app);

    match app.subcommand() {
        ("checkdb", Some(sub_m)) => {
            info!("Performing check db");
            match run_checkdb(pool,sub_m.is_present("simulate")) {
                Ok(_) => {},
                Err(e) => error!("Error at checkdb: {}",e)
            }
        },
        ("fcrawl", _) => {
            info!("Performing force crawl");
            let local_pool = &*pool;
            let local_config = &*config;
            let rt_time = NaiveTime::from_num_seconds_from_midnight(20,0);
            debug!("Result: {:?}",run_update(local_pool,local_config, &rt_time));
        },
        ("mail-test", _) => {
            info!("Sending test mail");
            send_mail(&*config,"Clantool test mail","This is a manually triggered test mail.");
        },
        _ =>  {
            info!("Entering daemon mode");
            run_timer(pool.clone(), config.clone(),&timer);
            loop {
                std::thread::sleep(std::time::Duration::from_millis(1000));
            }
        }
    }
}

/// Check DB for missing entries
fn run_checkdb(pool: Arc<Pool>, simulate: bool) -> Result<(),Error> {
    let mut conn = pool.get_conn()?;
    let missing_dates = db::get_missing_dates(&mut conn)?;

    if simulate {
        info!("Simulation mode, discarding result");
    } else {
        db::insert_missing_dates(&mut conn)?;
    }

    for date in missing_dates {
        info!("Missing: {}",date);
    }

    Ok(())
}

/// Initialize timed task
fn run_timer<'a>(pool: Arc<Pool>, config: Arc<Config>, timer: &'a timer::Timer) {
    let date_time = Local::now(); // get current datetime
    let today = Local::today();
    let target_naive_time = match NaiveTime::parse_from_str(&config.main.time, "%H:%M") {
        Ok(v) => v,
        Err(e) => {error!("Unable to parse config time!: {}",e); panic!();}
    };

    // get retry time
    let retry_time: NaiveTime = match NaiveTime::parse_from_str(&config.main.retry_interval, "%H:%M") {
        Ok(v) => v,
        Err(e) => {error!("Unable to parse config retry time!: {}",e); panic!();}
    };

    let schedule_time;
    trace!("Parsed time: {}",target_naive_time);
    if target_naive_time < date_time.time() {
        debug!("Target time is tinier then current time");
        // create from chrono::Duration, convert to std::time::Duration to add
        let c_duration = chrono::Duration::hours(INTERVALL_H);
        let tomorrow = today.checked_add_signed(c_duration).unwrap();
        schedule_time = tomorrow.and_time(target_naive_time).unwrap();
    } else {
        schedule_time = today.and_time(target_naive_time).unwrap();
    }
    info!("First execution will be on {}",schedule_time);

    let a = timer.schedule(schedule_time,Some(chrono::Duration::hours(INTERVALL_H)), move || {
        run_update(&*pool, &*config, &retry_time);
    });
    a.ignore();
}

/// Performs a complete crawl
fn run_update(pool: &Pool, config: &Config, retry_time: &NaiveTime) {
    trace!("performing crawler");

    let mut member_success  = false;
    let mut clan_success = false;

    for x in 1..config.main.retries+1 {
        let time = Local::now().naive_local();

        match run_update_member(pool,config, &time) {
            Ok(_) => { debug!("Member crawling successfull."); member_success = true;
                },
            Err(e) => error!("Error at member update: {}: {}",x,e),
        }

        match run_update_clan(pool, config, &time) {
            Ok(_) => { debug!("Clan crawling successfull."); clan_success = true;
                },
            Err(e) => error!("Error at clan update: {}: {}",x,e),
        }

        if member_success && clan_success {
            info!("Crawling successfull");
            break;
        } else {
            if x == config.main.retries {
                warn!("No dataset for this schedule, all retries failed!");
                match write_missing(&time,pool) {
                    Ok(_) => {},
                    Err(e) => error!("Unable to write missing date! {}",e),
                }

                if config.main.send_error_mail {
                    send_mail(config,"Clantool error",
                        "Error at clantool update execution!");
                }
            }else{
                std::thread::sleep(std::time::Duration::from_secs(retry_time.num_seconds_from_midnight().into()));
            }
        }
    }
}

/// Send email, catch & log errors
fn send_mail(config: &Config, subject: &str, message: &str) {
    match email::send_new::<Vec<&str>>(
        &config.main.mail_from,
        config.main.mail.iter().map(|v| &v[..]).collect(),
        // Subject
        subject,
        // Body
        message
    ) {
        Err(e) => error!("Error at mail sending: {}",e),
        _ => ()
    }
}

/// wrapper to write missing date
/// allowing for error return
fn write_missing(timestamp: &NaiveDateTime, pool: &Pool) -> Result<(),Error> {
    db::insert_missing_entry(timestamp,&mut pool.get_conn()?)?;
    Ok(())
}

/// get member url for ajax request
fn get_member_url(site: &u8, config: &Config) -> String {
    let _site = format!("{}",site);
    let end_row = site*config.main.clan_ajax_exptected_per_site;
    let start_row = end_row - (config.main.clan_ajax_exptected_per_site -1);
    let _start_row = format!("{}", start_row);
    let _end_row = format!("{}", end_row);
    let mut output = config.main.clan_ajax_url.replace(&config.main.clan_ajax_site_key, &_site);
    output = output.replace(&config.main.clan_ajax_start_row_key, &_start_row);
    output = output.replace(&config.main.clan_ajax_end_row_key, &_end_row);
    debug!("Start: {} end: {} site: {}",start_row,end_row,site);
    output
}

/// run member data crawl & update
fn run_update_member(pool: &Pool, config: &Config, time: &NaiveDateTime) -> Result<(),Error> {
    trace!("run_update_member");
    let mut site = 0;
    let mut members = Vec::new();
    let mut to_receive = 100;
    while members.len() < to_receive {
        site += 1;
        if site > config.main.clan_ajax_max_sites {
            error!("Reaching site {}, aborting.",site);
            return Err(Error::Other("Site over limit."));
        }
        let raw_members_json = try!(http::get(&get_member_url(&site,config),HeaderType::Ajax));
        let (mut members_temp,t_total) = try!(parser::parse_all_member(&raw_members_json));
        to_receive = t_total as usize;
        members.append(&mut members_temp);
        trace!("fetched site {}",site);
    }
    debug!("Fetched {} member entries",members.len());
    try!(db::insert_members(&mut pool.get_conn()?,&members,time));
    Ok(())
}

/// run clan crawl & update
fn run_update_clan(pool: &Pool, config: &Config, time: &NaiveDateTime) -> Result<(),Error> {
    trace!("run_update_clan");
    let raw_http_clan = try!(http::get(&config.main.clan_url,HeaderType::Html));
    let clan = try!(parser::parse_clan(&raw_http_clan));
    try!(db::insert_clan_update(&mut pool.get_conn()?,&clan,&time));
    Ok(())
}

/// Init log system
/// Creating a default log file if not existing
fn init_log() -> Result<(),Error> {
    let mut log_path = try!(get_executable_folder());
    log_path.push(LOG_PATH);
    let mut log_dir = log_path.clone();
    println!("LogPath: {:?}",&log_path);
    log_dir.pop();
    try!(DirBuilder::new()
    .recursive(true)
    .create(log_dir));

    if !metadata(&log_path).is_ok() {
        let config = include_str!("../log.yml");
        let mut file = try!(File::create(&log_path));
        try!(file.write_all(config.as_bytes()));
    }
    try!(log4rs::init_file(log_path, Default::default()));
    Ok(())
}

/// Returns the current executable folder
pub fn get_executable_folder() -> Result<std::path::PathBuf, Error> {
    let mut folder = try!(current_exe());
    folder.pop();
    Ok(folder)
}

/// Clan data structure
#[derive(Debug,PartialEq, PartialOrd)]
pub struct Clan {
    members: u8,
    wins: u16,
    losses: u16,
    draws: u16
}

/// Member data structure
#[derive(Debug,PartialEq, PartialOrd,Clone)]
pub struct Member {
    name: String,
    id: u32,
    exp: u32,
    contribution: u32
}

#[cfg(test)]
mod tests {

    use chrono::naive::NaiveTime;
    use chrono::Local;

    #[test]
    fn test_chrono() {
        let date = Local::today();
        let parsed_time = NaiveTime::parse_from_str("12:00","%H:%M").unwrap();
        let parsed_time_2 = NaiveTime::parse_from_str("12:01","%H:%M").unwrap();
        let datetime_1 = date.and_time(parsed_time).unwrap();
        let datetime_2 = date.and_time(parsed_time_2).unwrap();
        //assert_eq!(datetime_1.cmp(datetime_2),Ordering::Less);
        assert_eq!(parsed_time_2 > parsed_time,true);
        assert_eq!(datetime_2 > datetime_1,true);
    }
}
<commit_msg>fix version<commit_after>/*
Copyright 2017 Aron Heinecke

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

extern crate reqwest;
extern crate json;
extern crate flate2;
#[macro_use]
extern crate log;
extern crate log4rs;
#[macro_use]
extern crate quick_error;
extern crate mysql;
extern crate regex;
extern crate chrono;
extern crate toml;
extern crate timer;
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate clap;
extern crate sendmail;

mod http;
mod error;
mod parser;
mod db;
mod config;

use http::HeaderType;

use std::fs::{File,metadata};
use std::env::current_exe;
use std::fs::DirBuilder;
use std::io::Write;

use std::sync::Arc;

use sendmail::email;

use chrono::naive::{NaiveTime,NaiveDateTime};
use chrono::offset::Local;
use chrono::Timelike;

use mysql::Pool;

use error::Error;

use config::Config;

use clap::{Arg,App,SubCommand};

const USER_AGENT: &'static str = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0";
const REFERER: &'static str = "http://crossfire.z8games.com/";
const CONFIG_PATH: &'static str = "config/config.toml";
const LOG_PATH: &'static str = "config/log.yml";
const INTERVALL_H: i64 = 24; // execute intervall

fn main() {
    match init_log() {
        Err(e) => println!("Error on config initialization: {}",e),
        Ok(_) => println!("Initialized log")
    }
    info!("Clan tools crawler v0.2.1");

    let config = Arc::new(config::init_config());
    let pool = match db::new(config.db.ip.clone(),
        config.db.port,
        config.db.user.clone(),
        config.db.password.clone(),
        config.db.db.clone()) {
        Err(e) => {error!("Can't connect to DB! {}",e); panic!();},
        Ok(v) => Arc::new(v)
    };
    let timer = timer::Timer::new();

    let app = App::new("Clantool")
                    .version("2.1")
                    .author("Aron H. <aron.heinecke@t-online.de>")
                    .about("Gathers statistics about CF-NA clans. Starts as daemon per default")
                    .subcommand(SubCommand::with_name("fcrawl")
                        .about("force run crawl & exit"))
                    .subcommand(SubCommand::with_name("mail-test")
                        .about("Test mail sending")
                        /*.arg(Arg::with_name("mail")
                            .help("Required mail address")
                            .takes_value(true)
                            .required(true))
                        */    )
                    .subcommand(SubCommand::with_name("checkdb")
                        .about("checks DB for missing entries or doubles and corrects those")
                        .arg(Arg::with_name("simulate")
                            .short("s")
                            .help("simulation mode, leaves DB unchanged")))
                    .get_matches();
    trace!("{:?}",app);

    match app.subcommand() {
        ("checkdb", Some(sub_m)) => {
            info!("Performing check db");
            match run_checkdb(pool,sub_m.is_present("simulate")) {
                Ok(_) => {},
                Err(e) => error!("Error at checkdb: {}",e)
            }
        },
        ("fcrawl", _) => {
            info!("Performing force crawl");
            let local_pool = &*pool;
            let local_config = &*config;
            let rt_time = NaiveTime::from_num_seconds_from_midnight(20,0);
            debug!("Result: {:?}",run_update(local_pool,local_config, &rt_time));
        },
        ("mail-test", _) => {
            info!("Sending test mail");
            send_mail(&*config,"Clantool test mail","This is a manually triggered test mail.");
        },
        _ =>  {
            info!("Entering daemon mode");
            run_timer(pool.clone(), config.clone(),&timer);
            loop {
                std::thread::sleep(std::time::Duration::from_millis(1000));
            }
        }
    }
}

/// Check DB for missing entries
fn run_checkdb(pool: Arc<Pool>, simulate: bool) -> Result<(),Error> {
    let mut conn = pool.get_conn()?;
    let missing_dates = db::get_missing_dates(&mut conn)?;

    if simulate {
        info!("Simulation mode, discarding result");
    } else {
        db::insert_missing_dates(&mut conn)?;
    }

    for date in missing_dates {
        info!("Missing: {}",date);
    }

    Ok(())
}

/// Initialize timed task
fn run_timer<'a>(pool: Arc<Pool>, config: Arc<Config>, timer: &'a timer::Timer) {
    let date_time = Local::now(); // get current datetime
    let today = Local::today();
    let target_naive_time = match NaiveTime::parse_from_str(&config.main.time, "%H:%M") {
        Ok(v) => v,
        Err(e) => {error!("Unable to parse config time!: {}",e); panic!();}
    };

    // get retry time
    let retry_time: NaiveTime = match NaiveTime::parse_from_str(&config.main.retry_interval, "%H:%M") {
        Ok(v) => v,
        Err(e) => {error!("Unable to parse config retry time!: {}",e); panic!();}
    };

    let schedule_time;
    trace!("Parsed time: {}",target_naive_time);
    if target_naive_time < date_time.time() {
        debug!("Target time is tinier then current time");
        // create from chrono::Duration, convert to std::time::Duration to add
        let c_duration = chrono::Duration::hours(INTERVALL_H);
        let tomorrow = today.checked_add_signed(c_duration).unwrap();
        schedule_time = tomorrow.and_time(target_naive_time).unwrap();
    } else {
        schedule_time = today.and_time(target_naive_time).unwrap();
    }
    info!("First execution will be on {}",schedule_time);

    let a = timer.schedule(schedule_time,Some(chrono::Duration::hours(INTERVALL_H)), move || {
        run_update(&*pool, &*config, &retry_time);
    });
    a.ignore();
}

/// Performs a complete crawl
fn run_update(pool: &Pool, config: &Config, retry_time: &NaiveTime) {
    trace!("performing crawler");

    let mut member_success  = false;
    let mut clan_success = false;

    for x in 1..config.main.retries+1 {
        let time = Local::now().naive_local();

        match run_update_member(pool,config, &time) {
            Ok(_) => { debug!("Member crawling successfull."); member_success = true;
                },
            Err(e) => error!("Error at member update: {}: {}",x,e),
        }

        match run_update_clan(pool, config, &time) {
            Ok(_) => { debug!("Clan crawling successfull."); clan_success = true;
                },
            Err(e) => error!("Error at clan update: {}: {}",x,e),
        }

        if member_success && clan_success {
            info!("Crawling successfull");
            break;
        } else {
            if x == config.main.retries {
                warn!("No dataset for this schedule, all retries failed!");
                match write_missing(&time,pool) {
                    Ok(_) => {},
                    Err(e) => error!("Unable to write missing date! {}",e),
                }

                if config.main.send_error_mail {
                    send_mail(config,"Clantool error",
                        "Error at clantool update execution!");
                }
            }else{
                std::thread::sleep(std::time::Duration::from_secs(retry_time.num_seconds_from_midnight().into()));
            }
        }
    }
}

/// Send email, catch & log errors
fn send_mail(config: &Config, subject: &str, message: &str) {
    match email::send_new::<Vec<&str>>(
        &config.main.mail_from,
        config.main.mail.iter().map(|v| &v[..]).collect(),
        // Subject
        subject,
        // Body
        message
    ) {
        Err(e) => error!("Error at mail sending: {}",e),
        _ => ()
    }
}

/// wrapper to write missing date
/// allowing for error return
fn write_missing(timestamp: &NaiveDateTime, pool: &Pool) -> Result<(),Error> {
    db::insert_missing_entry(timestamp,&mut pool.get_conn()?)?;
    Ok(())
}

/// get member url for ajax request
fn get_member_url(site: &u8, config: &Config) -> String {
    let _site = format!("{}",site);
    let end_row = site*config.main.clan_ajax_exptected_per_site;
    let start_row = end_row - (config.main.clan_ajax_exptected_per_site -1);
    let _start_row = format!("{}", start_row);
    let _end_row = format!("{}", end_row);
    let mut output = config.main.clan_ajax_url.replace(&config.main.clan_ajax_site_key, &_site);
    output = output.replace(&config.main.clan_ajax_start_row_key, &_start_row);
    output = output.replace(&config.main.clan_ajax_end_row_key, &_end_row);
    debug!("Start: {} end: {} site: {}",start_row,end_row,site);
    output
}

/// run member data crawl & update
fn run_update_member(pool: &Pool, config: &Config, time: &NaiveDateTime) -> Result<(),Error> {
    trace!("run_update_member");
    let mut site = 0;
    let mut members = Vec::new();
    let mut to_receive = 100;
    while members.len() < to_receive {
        site += 1;
        if site > config.main.clan_ajax_max_sites {
            error!("Reaching site {}, aborting.",site);
            return Err(Error::Other("Site over limit."));
        }
        let raw_members_json = try!(http::get(&get_member_url(&site,config),HeaderType::Ajax));
        let (mut members_temp,t_total) = try!(parser::parse_all_member(&raw_members_json));
        to_receive = t_total as usize;
        members.append(&mut members_temp);
        trace!("fetched site {}",site);
    }
    debug!("Fetched {} member entries",members.len());
    try!(db::insert_members(&mut pool.get_conn()?,&members,time));
    Ok(())
}

/// run clan crawl & update
fn run_update_clan(pool: &Pool, config: &Config, time: &NaiveDateTime) -> Result<(),Error> {
    trace!("run_update_clan");
    let raw_http_clan = try!(http::get(&config.main.clan_url,HeaderType::Html));
    let clan = try!(parser::parse_clan(&raw_http_clan));
    try!(db::insert_clan_update(&mut pool.get_conn()?,&clan,&time));
    Ok(())
}

/// Init log system
/// Creating a default log file if not existing
fn init_log() -> Result<(),Error> {
    let mut log_path = try!(get_executable_folder());
    log_path.push(LOG_PATH);
    let mut log_dir = log_path.clone();
    println!("LogPath: {:?}",&log_path);
    log_dir.pop();
    try!(DirBuilder::new()
    .recursive(true)
    .create(log_dir));

    if !metadata(&log_path).is_ok() {
        let config = include_str!("../log.yml");
        let mut file = try!(File::create(&log_path));
        try!(file.write_all(config.as_bytes()));
    }
    try!(log4rs::init_file(log_path, Default::default()));
    Ok(())
}

/// Returns the current executable folder
pub fn get_executable_folder() -> Result<std::path::PathBuf, Error> {
    let mut folder = try!(current_exe());
    folder.pop();
    Ok(folder)
}

/// Clan data structure
#[derive(Debug,PartialEq, PartialOrd)]
pub struct Clan {
    members: u8,
    wins: u16,
    losses: u16,
    draws: u16
}

/// Member data structure
#[derive(Debug,PartialEq, PartialOrd,Clone)]
pub struct Member {
    name: String,
    id: u32,
    exp: u32,
    contribution: u32
}

#[cfg(test)]
mod tests {

    use chrono::naive::NaiveTime;
    use chrono::Local;

    #[test]
    fn test_chrono() {
        let date = Local::today();
        let parsed_time = NaiveTime::parse_from_str("12:00","%H:%M").unwrap();
        let parsed_time_2 = NaiveTime::parse_from_str("12:01","%H:%M").unwrap();
        let datetime_1 = date.and_time(parsed_time).unwrap();
        let datetime_2 = date.and_time(parsed_time_2).unwrap();
        //assert_eq!(datetime_1.cmp(datetime_2),Ordering::Less);
        assert_eq!(parsed_time_2 > parsed_time,true);
        assert_eq!(datetime_2 > datetime_1,true);
    }
}
<|endoftext|>


<commit_before>extern crate termion;
extern crate thread_scoped;
extern crate time;
extern crate voodoo;

mod info_view;
mod map_view;
mod level;
mod program;

use std::io::{Write};
use std::sync::mpsc::channel;

use termion::event::{Key, Event, MouseEvent};
use termion::input::{TermRead};

use voodoo::color::ColorValue;
use voodoo::window::{Point, TermCell, Window};

use info_view::InfoView;
use map_view::MapView;
use level::Level;
use program::{Program, ProgramRef};

const LEVEL_DESCR: [&'static str; 22] = [
    "                                                          ",
    "                                                          ",
    "                                                          ",
    "                                                          ",
    "          .........................                       ",
    "          .........................                       ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "   o................             .....                    ",
    "   o................             .....                    ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "          .........................                       ",
    "          .........................                       ",
    "                                                          ",
    "                                                          ",
    "                                                          ",
];

enum UiState {
    Unselected,
    Selected,
    Damage(ProgramRef, usize),
}

enum UiEvent {
    Quit,
    Tick,
    Test,
    Click(Point),
    // Movement
}

enum GameState {
    Setup,
    PlayerTurn,
    AITurn,
    AIExecute,
    Quit,
}

struct UiModelView {
    info: InfoView,
    map: MapView,
}

impl UiState {
    fn next(self, event: UiEvent, level: &mut Level, mv: &mut UiModelView) -> UiState {
        use UiEvent::*;
        use UiState::*;

        let UiModelView { ref mut info, ref mut map } = *mv;

        match (self, event) {
            (Unselected, Click(p)) => {
                for program in level.player_programs.iter() {
                    if program.borrow().intersects(p) {
                        map.highlight(program.clone(), &level);
                        info.display_program(&program.borrow());
                        return Selected;
                    }
                }
                Unselected
            }
            (Selected, Click(p)) => {
                let result = map.translate_click(p);
                if let Some(p) = result {
                    if let Some(ref mut program) = map.get_highlight() {
                        program.borrow_mut().move_to(p);
                    }
                    map.update_highlight(&level);
                    Selected
                }
                else {
                    map.clear_highlight();
                    info.clear();
                    Unselected
                }
            }
            (Damage(program, damage), Tick) => {
                if damage == 0 {
                    if map.get_highlight().is_some() {
                        Selected
                    }
                    else {
                        Unselected
                    }
                }
                else {
                    let new_ref = program.clone();
                    let position = { program.borrow().position };
                    let lived = { program.borrow_mut().damage() };
                    if lived {
                        Damage(new_ref, damage - 1)
                    }
                    else {
                        level.remove_program_at(position);
                        map.clear_highlight();
                        Damage(new_ref, 0)
                    }
                }
            }
            (Damage(program, damage), _) => {
                Damage(program, damage)
            }
            (state, Tick) => { state }
            (Selected, Test) => {
                if let Some(ref program) = map.get_highlight() {
                    Damage(program.clone(), 2)
                }
                else {
                    Selected
                }
            }
            (state, Test) => { state },
            (state, Quit) => { state },
        }
    }
}

struct GameModelView {
    ui_modelview: UiModelView,
}

struct State(GameState, UiState);

impl State {
    fn translate_event(&self, event: termion::event::Event, level: &mut Level, mv: &mut GameModelView) -> Option<UiEvent> {
        use GameState::*;
        match (self, event) {
            (_, Event::Key(Key::Char('q'))) => Some(UiEvent::Quit),
            (&State(PlayerTurn, _), Event::Key(Key::Char(' '))) => Some(UiEvent::Test),
            (&State(PlayerTurn, _), Event::Mouse(MouseEvent::Press(_, x, y))) => {
                if let Some(p) = mv.ui_modelview.map.from_global_frame(Point::new(x, y)) {
                    Some(UiEvent::Click(p))
                }
                else {
                    None
                }
            }
            _ => None,
        }
    }

    fn next(self, event: termion::event::Event, level: &mut Level, mv: &mut GameModelView) -> State {
        use GameState::*;
        if let Some(event) = self.translate_event(event, level, mv) {
            match self {
                State(PlayerTurn, ui) => Self::next_player_turn(ui, event, level, mv),
                _ => unimplemented!(),
            }
        }
        else {
            self
        }
    }

    fn tick(self, level: &mut Level, mv: &mut GameModelView) -> State {
        use GameState::*;
        match self {
            State(PlayerTurn, ui) => Self::next_player_turn(ui, UiEvent::Tick, level, mv),
            _ => unimplemented!(),
        }
    }

    fn next_player_turn(ui_state: UiState, event: UiEvent, level: &mut Level, mv: &mut GameModelView) -> State {
        use GameState::*;
        match event {
            UiEvent::Quit => State(Quit, ui_state),
            click@UiEvent::Click(_) => {
                State(PlayerTurn, ui_state.next(click, level, &mut mv.ui_modelview))
            }
            UiEvent::Tick | UiEvent::Test => {
                State(PlayerTurn, ui_state.next(event, level, &mut mv.ui_modelview))
            }
        }
    }
}

fn main() {
    use std::sync::mpsc::TryRecvError::*;
    use std::thread;
    use std::time::Duration;

    use voodoo::terminal::{Mode, Terminal};

    let mut level = Level::new(&LEVEL_DESCR);
    level.add_player_program(Program::new(Point::new(11, 11), "Hack"));
    level.add_player_program(Program::new(Point::new(5, 12), "Hack"));
    level.add_enemy_program(Program::new(Point::new(7, 12), "Hack"));

    let mut terminal = Terminal::new();
    terminal.cursor(Mode::Disabled);
    terminal.clear_color(ColorValue::Black);
    let Terminal { ref mut stdin, ref mut stdout } = terminal;

    stdout.flush().unwrap();

    let mut info = voodoo::window::Window::new(Point::new(0, 0), 20, 24);
    let mut map = voodoo::window::Window::new(Point::new(20, 0), 60, 24);
    info.border();
    map.border();

    let mut info_view = InfoView::new(info);
    let mut map_view = MapView::new(map);
    info_view.refresh(stdout);
    map_view.display(&level);
    map_view.refresh(stdout);

    let mut ui_modelview = UiModelView {
        info: info_view,
        map: map_view,
    };
    let mut ui_state = UiState::Unselected;

    let mut state = State(GameState::PlayerTurn, ui_state);
    let mut mv = GameModelView {
        ui_modelview: ui_modelview,
    };

    let (tx, rx) = channel();
    let guard = unsafe {
        thread_scoped::scoped(move || {
            for c in stdin.events() {
                let evt = c.unwrap();
                if let Event::Key(Key::Char('q')) = evt {
                    break;
                }
                tx.send(evt).unwrap();
            }
        })
    };

    let mut t = time::precise_time_ns();
    let mut dt = 0;

    'main: loop {
        loop {
            // Handle all pending events
            let msg = rx.try_recv();
            match msg {
                Ok(evt) => {
                    state = state.next(evt, &mut level, &mut mv);
                    if let State(GameState::Quit, _) = state {
                        break 'main;
                    }
                },
                Err(Disconnected) => break 'main,
                Err(Empty) => break,
            }
        }

        let now = time::precise_time_ns();
        dt += now - t;

        // TODO: use constant
        while dt >= 100000000 {
            state = state.tick(&mut level, &mut mv);
            dt -= 100000000;
        }

        mv.ui_modelview.info.refresh(stdout);
        mv.ui_modelview.map.display(&level);
        mv.ui_modelview.map.refresh(stdout);
        t = now;

        thread::sleep(Duration::from_millis(100 - dt / 1000000));
    }
    guard.join();
}
<commit_msg>fix: clear info after killing program<commit_after>extern crate termion;
extern crate thread_scoped;
extern crate time;
extern crate voodoo;

mod info_view;
mod map_view;
mod level;
mod program;

use std::io::{Write};
use std::sync::mpsc::channel;

use termion::event::{Key, Event, MouseEvent};
use termion::input::{TermRead};

use voodoo::color::ColorValue;
use voodoo::window::{Point, TermCell, Window};

use info_view::InfoView;
use map_view::MapView;
use level::Level;
use program::{Program, ProgramRef};

const LEVEL_DESCR: [&'static str; 22] = [
    "                                                          ",
    "                                                          ",
    "                                                          ",
    "                                                          ",
    "          .........................                       ",
    "          .........................                       ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "   o................             .....                    ",
    "   o................             .....                    ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "          ..                     ..                       ",
    "          .........................                       ",
    "          .........................                       ",
    "                                                          ",
    "                                                          ",
    "                                                          ",
];

enum UiState {
    Unselected,
    Selected,
    Damage(ProgramRef, usize),
}

enum UiEvent {
    Quit,
    Tick,
    Test,
    Click(Point),
    // Movement
}

enum GameState {
    Setup,
    PlayerTurn,
    AITurn,
    AIExecute,
    Quit,
}

struct UiModelView {
    info: InfoView,
    map: MapView,
}

impl UiState {
    fn next(self, event: UiEvent, level: &mut Level, mv: &mut UiModelView) -> UiState {
        use UiEvent::*;
        use UiState::*;

        let UiModelView { ref mut info, ref mut map } = *mv;

        match (self, event) {
            (Unselected, Click(p)) => {
                for program in level.player_programs.iter() {
                    if program.borrow().intersects(p) {
                        map.highlight(program.clone(), &level);
                        info.display_program(&program.borrow());
                        return Selected;
                    }
                }
                Unselected
            }
            (Selected, Click(p)) => {
                let result = map.translate_click(p);
                if let Some(p) = result {
                    if let Some(ref mut program) = map.get_highlight() {
                        program.borrow_mut().move_to(p);
                    }
                    map.update_highlight(&level);
                    Selected
                }
                else {
                    map.clear_highlight();
                    info.clear();
                    Unselected
                }
            }
            (Damage(program, damage), Tick) => {
                if damage == 0 {
                    if map.get_highlight().is_some() {
                        Selected
                    }
                    else {
                        info.clear();
                        Unselected
                    }
                }
                else {
                    let new_ref = program.clone();
                    let position = { program.borrow().position };
                    let lived = { program.borrow_mut().damage() };
                    if lived {
                        Damage(new_ref, damage - 1)
                    }
                    else {
                        level.remove_program_at(position);
                        map.clear_highlight();
                        Damage(new_ref, 0)
                    }
                }
            }
            (Damage(program, damage), _) => {
                Damage(program, damage)
            }
            (state, Tick) => { state }
            (Selected, Test) => {
                if let Some(ref program) = map.get_highlight() {
                    Damage(program.clone(), 2)
                }
                else {
                    Selected
                }
            }
            (state, Test) => { state },
            (state, Quit) => { state },
        }
    }
}

struct GameModelView {
    ui_modelview: UiModelView,
}

struct State(GameState, UiState);

impl State {
    fn translate_event(&self, event: termion::event::Event, level: &mut Level, mv: &mut GameModelView) -> Option<UiEvent> {
        use GameState::*;
        match (self, event) {
            (_, Event::Key(Key::Char('q'))) => Some(UiEvent::Quit),
            (&State(PlayerTurn, _), Event::Key(Key::Char(' '))) => Some(UiEvent::Test),
            (&State(PlayerTurn, _), Event::Mouse(MouseEvent::Press(_, x, y))) => {
                if let Some(p) = mv.ui_modelview.map.from_global_frame(Point::new(x, y)) {
                    Some(UiEvent::Click(p))
                }
                else {
                    None
                }
            }
            _ => None,
        }
    }

    fn next(self, event: termion::event::Event, level: &mut Level, mv: &mut GameModelView) -> State {
        use GameState::*;
        if let Some(event) = self.translate_event(event, level, mv) {
            match self {
                State(PlayerTurn, ui) => Self::next_player_turn(ui, event, level, mv),
                _ => unimplemented!(),
            }
        }
        else {
            self
        }
    }

    fn tick(self, level: &mut Level, mv: &mut GameModelView) -> State {
        use GameState::*;
        match self {
            State(PlayerTurn, ui) => Self::next_player_turn(ui, UiEvent::Tick, level, mv),
            _ => unimplemented!(),
        }
    }

    fn next_player_turn(ui_state: UiState, event: UiEvent, level: &mut Level, mv: &mut GameModelView) -> State {
        use GameState::*;
        match event {
            UiEvent::Quit => State(Quit, ui_state),
            click@UiEvent::Click(_) => {
                State(PlayerTurn, ui_state.next(click, level, &mut mv.ui_modelview))
            }
            UiEvent::Tick | UiEvent::Test => {
                State(PlayerTurn, ui_state.next(event, level, &mut mv.ui_modelview))
            }
        }
    }
}

fn main() {
    use std::sync::mpsc::TryRecvError::*;
    use std::thread;
    use std::time::Duration;

    use voodoo::terminal::{Mode, Terminal};

    let mut level = Level::new(&LEVEL_DESCR);
    level.add_player_program(Program::new(Point::new(11, 11), "Hack"));
    level.add_player_program(Program::new(Point::new(5, 12), "Hack"));
    level.add_enemy_program(Program::new(Point::new(7, 12), "Hack"));

    let mut terminal = Terminal::new();
    terminal.cursor(Mode::Disabled);
    terminal.clear_color(ColorValue::Black);
    let Terminal { ref mut stdin, ref mut stdout } = terminal;

    stdout.flush().unwrap();

    let mut info = voodoo::window::Window::new(Point::new(0, 0), 20, 24);
    let mut map = voodoo::window::Window::new(Point::new(20, 0), 60, 24);
    info.border();
    map.border();

    let mut info_view = InfoView::new(info);
    let mut map_view = MapView::new(map);
    info_view.refresh(stdout);
    map_view.display(&level);
    map_view.refresh(stdout);

    let mut ui_modelview = UiModelView {
        info: info_view,
        map: map_view,
    };
    let mut ui_state = UiState::Unselected;

    let mut state = State(GameState::PlayerTurn, ui_state);
    let mut mv = GameModelView {
        ui_modelview: ui_modelview,
    };

    let (tx, rx) = channel();
    let guard = unsafe {
        thread_scoped::scoped(move || {
            for c in stdin.events() {
                let evt = c.unwrap();
                if let Event::Key(Key::Char('q')) = evt {
                    break;
                }
                tx.send(evt).unwrap();
            }
        })
    };

    let mut t = time::precise_time_ns();
    let mut dt = 0;

    'main: loop {
        loop {
            // Handle all pending events
            let msg = rx.try_recv();
            match msg {
                Ok(evt) => {
                    state = state.next(evt, &mut level, &mut mv);
                    if let State(GameState::Quit, _) = state {
                        break 'main;
                    }
                },
                Err(Disconnected) => break 'main,
                Err(Empty) => break,
            }
        }

        let now = time::precise_time_ns();
        dt += now - t;

        // TODO: use constant
        while dt >= 100000000 {
            state = state.tick(&mut level, &mut mv);
            dt -= 100000000;
        }

        mv.ui_modelview.info.refresh(stdout);
        mv.ui_modelview.map.display(&level);
        mv.ui_modelview.map.refresh(stdout);
        t = now;

        thread::sleep(Duration::from_millis(100 - dt / 1000000));
    }
    guard.join();
}
<|endoftext|>


<commit_before>#![forbid(unsafe_code)]

pub mod api;
pub mod db;
pub mod importer;
pub mod indexer;
pub mod model;
pub mod opening;
pub mod util;

use std::{mem, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};

use axum::{
    extract::{Extension, Path, Query},
    handler::{get, put},
    http::StatusCode,
    AddExtensionLayer, Json, Router,
};
use clap::Parser;
use futures_util::stream::Stream;
use serde::Deserialize;
use serde_with::{serde_as, DisplayFromStr};
use shakmaty::{
    fen::Fen,
    san::{San, SanPlus},
    uci::Uci,
    variant::{Variant, VariantPosition},
    zobrist::Zobrist,
    CastlingMode,
};
use tokio::sync::watch;

use crate::{
    api::{
        Error, ExplorerGame, ExplorerGameWithUci, ExplorerMove, ExplorerResponse, LichessQuery,
        Limits, MasterQuery, NdJson, PersonalQuery, PersonalQueryFilter,
    },
    db::{Database, QueryableDatabase},
    importer::{LichessGame, LichessImporter, MasterImporter},
    indexer::{IndexerOpt, IndexerStub},
    model::{GameId, KeyBuilder, KeyPrefix, MasterGame, MasterGameWithId, PreparedMove, UserId},
    opening::{Opening, Openings},
    util::DedupStreamExt as _,
};

#[derive(Parser)]
struct Opt {
    #[clap(long, default_value = "127.0.0.1:9000")]
    bind: SocketAddr,
    #[clap(long, default_value = "_db")]
    db: PathBuf,
    #[clap(long)]
    cors: bool,
    #[clap(flatten)]
    indexer: IndexerOpt,
}

#[tokio::main]
async fn main() {
    env_logger::Builder::from_env(
        env_logger::Env::new()
            .filter("EXPLORER_LOG")
            .write_style("EXPLORER_LOG_STYLE"),
    )
    .format_timestamp(None)
    .format_module_path(false)
    .format_target(false)
    .init();

    let opt = Opt::parse();

    let openings: &'static Openings = Box::leak(Box::new(Openings::build_table()));
    let db = Arc::new(Database::open(opt.db).expect("db"));
    let (indexer, join_handles) = IndexerStub::spawn(Arc::clone(&db), opt.indexer);
    let master_importer = MasterImporter::new(Arc::clone(&db));
    let lichess_importer = LichessImporter::new(Arc::clone(&db));

    let app = Router::new()
        .route("/admin/db/:prop", get(db_property))
        .route("/admin/game/:prop", get(game_property))
        .route("/admin/personal/:prop", get(personal_property))
        .route("/admin/explorer.indexing", get(num_indexing))
        .route("/import/master", put(master_import))
        .route("/import/lichess", put(lichess_import))
        .route("/master/pgn/:id", get(master_pgn))
        .route("/master", get(master))
        .route("/personal", get(personal))
        .route("/lichess", get(lichess))
        .layer(AddExtensionLayer::new(openings))
        .layer(AddExtensionLayer::new(db))
        .layer(AddExtensionLayer::new(master_importer))
        .layer(AddExtensionLayer::new(lichess_importer))
        .layer(AddExtensionLayer::new(indexer));

    let app = if opt.cors {
        app.layer(tower_http::set_header::SetResponseHeaderLayer::<
            _,
            axum::body::Body,
        >::if_not_present(
            axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN,
            axum::http::HeaderValue::from_static("*"),
        ))
    } else {
        app
    };

    axum::Server::bind(&opt.bind)
        .serve(app.into_make_service())
        .await
        .expect("bind");

    for join_handle in join_handles {
        join_handle.await.expect("indexer");
    }
}

async fn db_property(
    Extension(db): Extension<Arc<Database>>,
    Path(prop): Path<String>,
) -> Result<String, StatusCode> {
    db.queryable()
        .db_property(&prop)
        .expect("get property")
        .ok_or(StatusCode::NOT_FOUND)
}

async fn game_property(
    Extension(db): Extension<Arc<Database>>,
    Path(prop): Path<String>,
) -> Result<String, StatusCode> {
    db.queryable()
        .game_property(&prop)
        .expect("get property")
        .ok_or(StatusCode::NOT_FOUND)
}

async fn personal_property(
    Extension(db): Extension<Arc<Database>>,
    Path(prop): Path<String>,
) -> Result<String, StatusCode> {
    db.queryable()
        .personal_property(&prop)
        .expect("get property")
        .ok_or(StatusCode::NOT_FOUND)
}

async fn num_indexing(Extension(indexer): Extension<IndexerStub>) -> String {
    indexer.num_indexing().await.to_string()
}

struct PersonalStreamState {
    indexing: Option<watch::Receiver<()>>,
    key: KeyPrefix,
    db: Arc<Database>,
    filter: PersonalQueryFilter,
    limits: Limits,
    pos: VariantPosition,
    opening: Option<&'static Opening>,
    first: bool,
    done: bool,
}

fn finalize_lichess_moves(
    moves: Vec<PreparedMove>,
    pos: &VariantPosition,
    queryable: &QueryableDatabase,
) -> Vec<ExplorerMove> {
    moves
        .into_iter()
        .map(|p| ExplorerMove {
            stats: p.stats,
            san: p.uci.to_move(pos).map_or(
                SanPlus {
                    san: San::Null,
                    suffix: None,
                },
                |m| SanPlus::from_move(pos.clone(), &m),
            ),
            uci: p.uci,
            average_rating: p.average_rating,
            average_opponent_rating: p.average_opponent_rating,
            game: p.game.and_then(|id| {
                queryable
                    .get_game_info(id)
                    .expect("get game")
                    .map(|info| ExplorerGame::from_lichess(id, info))
            }),
        })
        .collect()
}

fn finalize_lichess_games(
    games: Vec<(Uci, GameId)>,
    queryable: &QueryableDatabase,
) -> Vec<ExplorerGameWithUci> {
    games
        .into_iter()
        .flat_map(|(uci, id)| {
            queryable
                .get_game_info(id)
                .expect("get game")
                .map(|info| ExplorerGameWithUci {
                    uci,
                    row: ExplorerGame::from_lichess(id, info),
                })
        })
        .collect()
}

async fn personal(
    Extension(openings): Extension<&'static Openings>,
    Extension(db): Extension<Arc<Database>>,
    Extension(indexer): Extension<IndexerStub>,
    Query(query): Query<PersonalQuery>,
) -> Result<NdJson<impl Stream<Item = ExplorerResponse>>, Error> {
    let player = UserId::from(query.player);
    let indexing = indexer.index_player(&player).await;

    let variant = query.variant.into();

    let mut pos = Zobrist::new(match query.fen {
        Some(fen) => VariantPosition::from_setup(variant, &Fen::from(fen), CastlingMode::Chess960)?,
        None => VariantPosition::new(variant),
    });

    let opening = openings.classify_and_play(&mut pos, query.play)?;

    let key = KeyBuilder::personal(&player, query.color).with_zobrist(variant, pos.zobrist_hash());

    let state = PersonalStreamState {
        filter: query.filter,
        limits: query.limits,
        db,
        indexing,
        opening,
        key,
        pos: pos.into_inner(),
        first: true,
        done: false,
    };

    Ok(NdJson(futures_util::stream::unfold(
        state,
        |mut state| async move {
            if state.done {
                return None;
            }

            let first = mem::replace(&mut state.first, false);
            state.done = match state.indexing {
                Some(ref mut indexing) => {
                    tokio::select! {
                        _ = indexing.changed() => true,
                        _ = tokio::time::sleep(Duration::from_millis(if first { 0 } else { 1000 })) => false,
                    }
                }
                None => true,
            };

            let queryable = state.db.queryable();
            let mut filtered = queryable
                .get_personal(&state.key, state.filter.since, state.filter.until)
                .expect("get personal")
                .prepare(&state.filter);

            filtered.moves.truncate(state.limits.moves.unwrap_or(usize::MAX));
            filtered.recent_games.truncate(state.limits.recent_games);

            Some((
                ExplorerResponse {
                    total: filtered.total,
                    moves: finalize_lichess_moves(filtered.moves, &state.pos, &queryable),
                    recent_games: Some(finalize_lichess_games(filtered.recent_games, &queryable)),
                    top_games: None,
                    opening: state.opening,
                },
                state,
            ))
        },
    ).dedup_by_key(|res| res.total.total())))
}

async fn master_import(
    Json(body): Json<MasterGameWithId>,
    Extension(importer): Extension<MasterImporter>,
) -> Result<(), Error> {
    importer.import(body).await
}

#[serde_as]
#[derive(Deserialize)]
struct MasterGameId(#[serde_as(as = "DisplayFromStr")] GameId);

async fn master_pgn(
    Path(MasterGameId(id)): Path<MasterGameId>,
    Extension(db): Extension<Arc<Database>>,
) -> Result<MasterGame, StatusCode> {
    match db.queryable().get_master_game(id).expect("get master game") {
        Some(game) => Ok(game),
        None => Err(StatusCode::NOT_FOUND),
    }
}

async fn master(
    Extension(openings): Extension<&'static Openings>,
    Extension(db): Extension<Arc<Database>>,
    Query(query): Query<MasterQuery>,
) -> Result<Json<ExplorerResponse>, Error> {
    let mut pos = Zobrist::new(match query.fen {
        Some(fen) => {
            VariantPosition::from_setup(Variant::Chess, &Fen::from(fen), CastlingMode::Chess960)?
        }
        None => VariantPosition::new(Variant::Chess),
    });

    let opening = openings.classify_and_play(&mut pos, query.play)?;
    let key = KeyBuilder::master().with_zobrist(Variant::Chess, pos.zobrist_hash());
    let queryable = db.queryable();
    let mut entry = queryable
        .get_master(key, query.since, query.until)
        .expect("get master")
        .prepare();

    entry.moves.truncate(query.limits.moves.unwrap_or(12));
    entry.top_games.truncate(query.limits.top_games);

    Ok(Json(ExplorerResponse {
        total: entry.total,
        moves: entry
            .moves
            .into_iter()
            .map(|p| ExplorerMove {
                san: p.uci.to_move(&pos).map_or(
                    SanPlus {
                        san: San::Null,
                        suffix: None,
                    },
                    |m| SanPlus::from_move(pos.clone(), &m),
                ),
                uci: p.uci,
                average_rating: p.average_rating,
                average_opponent_rating: p.average_opponent_rating,
                stats: p.stats,
                game: p.game.and_then(|id| {
                    queryable
                        .get_master_game(id)
                        .expect("get master game")
                        .map(|info| ExplorerGame::from_master(id, info))
                }),
            })
            .collect(),
        top_games: Some(
            entry
                .top_games
                .into_iter()
                .flat_map(|(uci, id)| {
                    queryable
                        .get_master_game(id)
                        .expect("get master game")
                        .map(|info| ExplorerGameWithUci {
                            uci,
                            row: ExplorerGame::from_master(id, info),
                        })
                })
                .collect(),
        ),
        opening,
        recent_games: None,
    }))
}

async fn lichess_import(
    Json(body): Json<Vec<LichessGame>>,
    Extension(importer): Extension<LichessImporter>,
) -> Result<(), Error> {
    for game in body {
        importer.import(game).await?;
    }
    Ok(())
}

async fn lichess(
    Extension(openings): Extension<&'static Openings>,
    Extension(db): Extension<Arc<Database>>,
    Query(query): Query<LichessQuery>,
) -> Result<Json<ExplorerResponse>, Error> {
    let variant = Variant::from(query.variant);
    let mut pos = Zobrist::new(match query.fen {
        Some(fen) => VariantPosition::from_setup(variant, &Fen::from(fen), CastlingMode::Chess960)?,
        None => VariantPosition::new(variant),
    });

    let opening = openings.classify_and_play(&mut pos, query.play)?;

    let key = KeyBuilder::lichess().with_zobrist(variant, pos.zobrist_hash());
    let queryable = db.queryable();
    let mut filtered = queryable
        .get_lichess(&key, query.filter.since, query.filter.until)
        .expect("get lichess")
        .prepare(&query.filter);

    filtered.moves.truncate(query.limits.moves.unwrap_or(12));
    filtered.recent_games.truncate(query.limits.recent_games);
    filtered.top_games.truncate(query.limits.recent_games);

    Ok(Json(ExplorerResponse {
        total: filtered.total,
        moves: finalize_lichess_moves(filtered.moves, pos.as_inner(), &queryable),
        recent_games: Some(finalize_lichess_games(filtered.recent_games, &queryable)),
        top_games: Some(finalize_lichess_games(filtered.top_games, &queryable)),
        opening,
    }))
}
<commit_msg>make routes match ExplorerDb<commit_after>#![forbid(unsafe_code)]

pub mod api;
pub mod db;
pub mod importer;
pub mod indexer;
pub mod model;
pub mod opening;
pub mod util;

use std::{mem, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};

use axum::{
    extract::{Extension, Path, Query},
    handler::{get, put},
    http::StatusCode,
    AddExtensionLayer, Json, Router,
};
use clap::Parser;
use futures_util::stream::Stream;
use serde::Deserialize;
use serde_with::{serde_as, DisplayFromStr};
use shakmaty::{
    fen::Fen,
    san::{San, SanPlus},
    uci::Uci,
    variant::{Variant, VariantPosition},
    zobrist::Zobrist,
    CastlingMode,
};
use tokio::sync::watch;

use crate::{
    api::{
        Error, ExplorerGame, ExplorerGameWithUci, ExplorerMove, ExplorerResponse, LichessQuery,
        Limits, MasterQuery, NdJson, PersonalQuery, PersonalQueryFilter,
    },
    db::{Database, QueryableDatabase},
    importer::{LichessGame, LichessImporter, MasterImporter},
    indexer::{IndexerOpt, IndexerStub},
    model::{GameId, KeyBuilder, KeyPrefix, MasterGame, MasterGameWithId, PreparedMove, UserId},
    opening::{Opening, Openings},
    util::DedupStreamExt as _,
};

#[derive(Parser)]
struct Opt {
    #[clap(long, default_value = "127.0.0.1:9000")]
    bind: SocketAddr,
    #[clap(long, default_value = "_db")]
    db: PathBuf,
    #[clap(long)]
    cors: bool,
    #[clap(flatten)]
    indexer: IndexerOpt,
}

#[tokio::main]
async fn main() {
    env_logger::Builder::from_env(
        env_logger::Env::new()
            .filter("EXPLORER_LOG")
            .write_style("EXPLORER_LOG_STYLE"),
    )
    .format_timestamp(None)
    .format_module_path(false)
    .format_target(false)
    .init();

    let opt = Opt::parse();

    let openings: &'static Openings = Box::leak(Box::new(Openings::build_table()));
    let db = Arc::new(Database::open(opt.db).expect("db"));
    let (indexer, join_handles) = IndexerStub::spawn(Arc::clone(&db), opt.indexer);
    let master_importer = MasterImporter::new(Arc::clone(&db));
    let lichess_importer = LichessImporter::new(Arc::clone(&db));

    let app = Router::new()
        .route("/admin/db/:prop", get(db_property))
        .route("/admin/game/:prop", get(game_property))
        .route("/admin/personal/:prop", get(personal_property))
        .route("/admin/explorer.indexing", get(num_indexing))
        .route("/import/masters", put(master_import))
        .route("/import/lichess", put(lichess_import))
        .route("/masters/pgn/:id", get(master_pgn))
        .route("/masters", get(master))
        .route("/master", get(master)) // bc
        .route("/personal", get(personal)) // bc
        .route("/player", get(personal))
        .route("/lichess", get(lichess))
        .layer(AddExtensionLayer::new(openings))
        .layer(AddExtensionLayer::new(db))
        .layer(AddExtensionLayer::new(master_importer))
        .layer(AddExtensionLayer::new(lichess_importer))
        .layer(AddExtensionLayer::new(indexer));

    let app = if opt.cors {
        app.layer(tower_http::set_header::SetResponseHeaderLayer::<
            _,
            axum::body::Body,
        >::if_not_present(
            axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN,
            axum::http::HeaderValue::from_static("*"),
        ))
    } else {
        app
    };

    axum::Server::bind(&opt.bind)
        .serve(app.into_make_service())
        .await
        .expect("bind");

    for join_handle in join_handles {
        join_handle.await.expect("indexer");
    }
}

async fn db_property(
    Extension(db): Extension<Arc<Database>>,
    Path(prop): Path<String>,
) -> Result<String, StatusCode> {
    db.queryable()
        .db_property(&prop)
        .expect("get property")
        .ok_or(StatusCode::NOT_FOUND)
}

async fn game_property(
    Extension(db): Extension<Arc<Database>>,
    Path(prop): Path<String>,
) -> Result<String, StatusCode> {
    db.queryable()
        .game_property(&prop)
        .expect("get property")
        .ok_or(StatusCode::NOT_FOUND)
}

async fn personal_property(
    Extension(db): Extension<Arc<Database>>,
    Path(prop): Path<String>,
) -> Result<String, StatusCode> {
    db.queryable()
        .personal_property(&prop)
        .expect("get property")
        .ok_or(StatusCode::NOT_FOUND)
}

async fn num_indexing(Extension(indexer): Extension<IndexerStub>) -> String {
    indexer.num_indexing().await.to_string()
}

struct PersonalStreamState {
    indexing: Option<watch::Receiver<()>>,
    key: KeyPrefix,
    db: Arc<Database>,
    filter: PersonalQueryFilter,
    limits: Limits,
    pos: VariantPosition,
    opening: Option<&'static Opening>,
    first: bool,
    done: bool,
}

fn finalize_lichess_moves(
    moves: Vec<PreparedMove>,
    pos: &VariantPosition,
    queryable: &QueryableDatabase,
) -> Vec<ExplorerMove> {
    moves
        .into_iter()
        .map(|p| ExplorerMove {
            stats: p.stats,
            san: p.uci.to_move(pos).map_or(
                SanPlus {
                    san: San::Null,
                    suffix: None,
                },
                |m| SanPlus::from_move(pos.clone(), &m),
            ),
            uci: p.uci,
            average_rating: p.average_rating,
            average_opponent_rating: p.average_opponent_rating,
            game: p.game.and_then(|id| {
                queryable
                    .get_game_info(id)
                    .expect("get game")
                    .map(|info| ExplorerGame::from_lichess(id, info))
            }),
        })
        .collect()
}

fn finalize_lichess_games(
    games: Vec<(Uci, GameId)>,
    queryable: &QueryableDatabase,
) -> Vec<ExplorerGameWithUci> {
    games
        .into_iter()
        .flat_map(|(uci, id)| {
            queryable
                .get_game_info(id)
                .expect("get game")
                .map(|info| ExplorerGameWithUci {
                    uci,
                    row: ExplorerGame::from_lichess(id, info),
                })
        })
        .collect()
}

async fn personal(
    Extension(openings): Extension<&'static Openings>,
    Extension(db): Extension<Arc<Database>>,
    Extension(indexer): Extension<IndexerStub>,
    Query(query): Query<PersonalQuery>,
) -> Result<NdJson<impl Stream<Item = ExplorerResponse>>, Error> {
    let player = UserId::from(query.player);
    let indexing = indexer.index_player(&player).await;

    let variant = query.variant.into();

    let mut pos = Zobrist::new(match query.fen {
        Some(fen) => VariantPosition::from_setup(variant, &Fen::from(fen), CastlingMode::Chess960)?,
        None => VariantPosition::new(variant),
    });

    let opening = openings.classify_and_play(&mut pos, query.play)?;

    let key = KeyBuilder::personal(&player, query.color).with_zobrist(variant, pos.zobrist_hash());

    let state = PersonalStreamState {
        filter: query.filter,
        limits: query.limits,
        db,
        indexing,
        opening,
        key,
        pos: pos.into_inner(),
        first: true,
        done: false,
    };

    Ok(NdJson(futures_util::stream::unfold(
        state,
        |mut state| async move {
            if state.done {
                return None;
            }

            let first = mem::replace(&mut state.first, false);
            state.done = match state.indexing {
                Some(ref mut indexing) => {
                    tokio::select! {
                        _ = indexing.changed() => true,
                        _ = tokio::time::sleep(Duration::from_millis(if first { 0 } else { 1000 })) => false,
                    }
                }
                None => true,
            };

            let queryable = state.db.queryable();
            let mut filtered = queryable
                .get_personal(&state.key, state.filter.since, state.filter.until)
                .expect("get personal")
                .prepare(&state.filter);

            filtered.moves.truncate(state.limits.moves.unwrap_or(usize::MAX));
            filtered.recent_games.truncate(state.limits.recent_games);

            Some((
                ExplorerResponse {
                    total: filtered.total,
                    moves: finalize_lichess_moves(filtered.moves, &state.pos, &queryable),
                    recent_games: Some(finalize_lichess_games(filtered.recent_games, &queryable)),
                    top_games: None,
                    opening: state.opening,
                },
                state,
            ))
        },
    ).dedup_by_key(|res| res.total.total())))
}

async fn master_import(
    Json(body): Json<MasterGameWithId>,
    Extension(importer): Extension<MasterImporter>,
) -> Result<(), Error> {
    importer.import(body).await
}

#[serde_as]
#[derive(Deserialize)]
struct MasterGameId(#[serde_as(as = "DisplayFromStr")] GameId);

async fn master_pgn(
    Path(MasterGameId(id)): Path<MasterGameId>,
    Extension(db): Extension<Arc<Database>>,
) -> Result<MasterGame, StatusCode> {
    match db.queryable().get_master_game(id).expect("get master game") {
        Some(game) => Ok(game),
        None => Err(StatusCode::NOT_FOUND),
    }
}

async fn master(
    Extension(openings): Extension<&'static Openings>,
    Extension(db): Extension<Arc<Database>>,
    Query(query): Query<MasterQuery>,
) -> Result<Json<ExplorerResponse>, Error> {
    let mut pos = Zobrist::new(match query.fen {
        Some(fen) => {
            VariantPosition::from_setup(Variant::Chess, &Fen::from(fen), CastlingMode::Chess960)?
        }
        None => VariantPosition::new(Variant::Chess),
    });

    let opening = openings.classify_and_play(&mut pos, query.play)?;
    let key = KeyBuilder::master().with_zobrist(Variant::Chess, pos.zobrist_hash());
    let queryable = db.queryable();
    let mut entry = queryable
        .get_master(key, query.since, query.until)
        .expect("get master")
        .prepare();

    entry.moves.truncate(query.limits.moves.unwrap_or(12));
    entry.top_games.truncate(query.limits.top_games);

    Ok(Json(ExplorerResponse {
        total: entry.total,
        moves: entry
            .moves
            .into_iter()
            .map(|p| ExplorerMove {
                san: p.uci.to_move(&pos).map_or(
                    SanPlus {
                        san: San::Null,
                        suffix: None,
                    },
                    |m| SanPlus::from_move(pos.clone(), &m),
                ),
                uci: p.uci,
                average_rating: p.average_rating,
                average_opponent_rating: p.average_opponent_rating,
                stats: p.stats,
                game: p.game.and_then(|id| {
                    queryable
                        .get_master_game(id)
                        .expect("get master game")
                        .map(|info| ExplorerGame::from_master(id, info))
                }),
            })
            .collect(),
        top_games: Some(
            entry
                .top_games
                .into_iter()
                .flat_map(|(uci, id)| {
                    queryable
                        .get_master_game(id)
                        .expect("get master game")
                        .map(|info| ExplorerGameWithUci {
                            uci,
                            row: ExplorerGame::from_master(id, info),
                        })
                })
                .collect(),
        ),
        opening,
        recent_games: None,
    }))
}

async fn lichess_import(
    Json(body): Json<Vec<LichessGame>>,
    Extension(importer): Extension<LichessImporter>,
) -> Result<(), Error> {
    for game in body {
        importer.import(game).await?;
    }
    Ok(())
}

async fn lichess(
    Extension(openings): Extension<&'static Openings>,
    Extension(db): Extension<Arc<Database>>,
    Query(query): Query<LichessQuery>,
) -> Result<Json<ExplorerResponse>, Error> {
    let variant = Variant::from(query.variant);
    let mut pos = Zobrist::new(match query.fen {
        Some(fen) => VariantPosition::from_setup(variant, &Fen::from(fen), CastlingMode::Chess960)?,
        None => VariantPosition::new(variant),
    });

    let opening = openings.classify_and_play(&mut pos, query.play)?;

    let key = KeyBuilder::lichess().with_zobrist(variant, pos.zobrist_hash());
    let queryable = db.queryable();
    let mut filtered = queryable
        .get_lichess(&key, query.filter.since, query.filter.until)
        .expect("get lichess")
        .prepare(&query.filter);

    filtered.moves.truncate(query.limits.moves.unwrap_or(12));
    filtered.recent_games.truncate(query.limits.recent_games);
    filtered.top_games.truncate(query.limits.recent_games);

    Ok(Json(ExplorerResponse {
        total: filtered.total,
        moves: finalize_lichess_moves(filtered.moves, pos.as_inner(), &queryable),
        recent_games: Some(finalize_lichess_games(filtered.recent_games, &queryable)),
        top_games: Some(finalize_lichess_games(filtered.top_games, &queryable)),
        opening,
    }))
}
<|endoftext|>


<commit_before>extern crate mqtt;
extern crate rs_jsonpath;
#[macro_use]
extern crate clap;
extern crate itertools;

use std::net::{TcpListener, TcpStream};
use std::thread;
use std::rc::Rc;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::sync::{Arc, Mutex};

use itertools::Itertools;

use mqtt::{Encodable, Decodable};
use mqtt::packet::{ConnackPacket, Packet, PingrespPacket, SubackPacket, VariablePacket};
use mqtt::{TopicFilter, QualityOfService};
use mqtt::control::{ConnectReturnCode};
use mqtt::packet::suback::{SubscribeReturnCode};

use rs_jsonpath::lookup;

fn main() {
    let matches = clap_app!(zink =>
                            (version: "0.1.0")
                            (@arg file: -f --file +takes_value "File to append result")
                            (@arg JSONPATH: +takes_value "JSON paths to use")
    ).get_matches();

    let jsonpaths: Vec<String> = matches.value_of("JSONPATH").unwrap().split(",").map(String::from).collect();

    let mut handle: Arc<Mutex<Write + Send>> = if let Some(filepath) = matches.value_of("file") {
        Arc::new(Mutex::new(OpenOptions::new()
                 .append(true)
                 .create(true)
                 .open(filepath)
                 .unwrap())) as Arc<Mutex<Write + Send>>
    } else {
        Arc::new(Mutex::new(io::stdout())) as Arc<Mutex<Write + Send>>
    };

    let listener = TcpListener::bind("127.0.0.1:1883").unwrap();

    println!("Listening on port 1883");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                let paths = jsonpaths.clone();
                let handle = handle.clone();
                thread::spawn(move || {
                    handle_client(stream, paths, handle);
                });
            }
            Err(e) => {
                println!("{}", e);
            }
        }
    }
}

fn sub_to_ack(&(_, qos): &(TopicFilter, QualityOfService)) -> SubscribeReturnCode {
    match qos {
        QualityOfService::Level0 => SubscribeReturnCode::MaximumQoSLevel0,
        QualityOfService::Level1 => SubscribeReturnCode::MaximumQoSLevel1,
        QualityOfService::Level2 => SubscribeReturnCode::MaximumQoSLevel1,
    }
}

fn handle_client(mut stream: TcpStream, jsonpaths: Vec<String>, handle: Arc<Mutex<Write>>) {
    // This makes .read() call blocking.
    // Otherwise, mqtt decode consumes 100% CPU time.
    stream.set_read_timeout(None);

    println!("{:?}", stream);
    let auto_decode = VariablePacket::decode(&mut stream);
    println!("{:?}", auto_decode);
    ConnackPacket::new(false, ConnectReturnCode::ConnectionAccepted).encode(&mut stream);

    loop {
        let parse_result = VariablePacket::decode(&mut stream);
        if let Ok(packet) = parse_result {
            println!("{:?}", packet);
            match packet {
                VariablePacket::SubscribePacket(x) => {
                    SubackPacket::new(
                        x.packet_identifier(),
                        x.payload().subscribes().into_iter().map(sub_to_ack).collect()
                    ).encode(&mut stream);
                }
                VariablePacket::PingreqPacket(_) => {
                    PingrespPacket::new()
                        .encode(&mut stream);
                }
                VariablePacket::PublishPacket(x) => {
                    if let Ok(payload) = std::str::from_utf8(x.payload()) {
                        println!("{}: {}", x.topic_name(), payload);

                        let mut csv = jsonpaths.iter()
                            .map(|path| {
                                let res = lookup(String::from(payload), path.clone()).unwrap_or("".to_string());
                                if res != "[]" { res } else { "".to_string() }
                            })
                            .join(",");
                        csv.push('\n');

                        let mut handle = handle.lock().unwrap();
                        handle.write_all(csv.as_bytes());
                    }
                }
                _ => {
                }
            }
        }
    }
}
<commit_msg>Move logs to stderr<commit_after>extern crate mqtt;
extern crate rs_jsonpath;
#[macro_use]
extern crate clap;
extern crate itertools;

use std::net::{TcpListener, TcpStream};
use std::thread;
use std::rc::Rc;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::sync::{Arc, Mutex};

use itertools::Itertools;

use mqtt::{Encodable, Decodable};
use mqtt::packet::{ConnackPacket, Packet, PingrespPacket, SubackPacket, VariablePacket};
use mqtt::{TopicFilter, QualityOfService};
use mqtt::control::{ConnectReturnCode};
use mqtt::packet::suback::{SubscribeReturnCode};

use rs_jsonpath::lookup;

macro_rules! log(
    ($($arg:tt)*) => { {
        writeln!(&mut ::std::io::stderr(), $($arg)*)
    } }
);

fn main() {
    let matches = clap_app!(zink =>
                            (version: "0.1.0")
                            (@arg file: -f --file +takes_value "File to append result")
                            (@arg JSONPATH: +takes_value "JSON paths to use")
    ).get_matches();

    let jsonpaths: Vec<String> = matches.value_of("JSONPATH").unwrap().split(",").map(String::from).collect();

    let mut handle: Arc<Mutex<Write + Send>> = if let Some(filepath) = matches.value_of("file") {
        Arc::new(Mutex::new(OpenOptions::new()
                 .append(true)
                 .create(true)
                 .open(filepath)
                 .unwrap())) as Arc<Mutex<Write + Send>>
    } else {
        Arc::new(Mutex::new(io::stdout())) as Arc<Mutex<Write + Send>>
    };

    let listener = TcpListener::bind("127.0.0.1:1883").unwrap();

    log!("Listening on port 1883");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                let paths = jsonpaths.clone();
                let handle = handle.clone();
                thread::spawn(move || {
                    handle_client(stream, paths, handle);
                });
            }
            Err(e) => {
                log!("{}", e);
            }
        }
    }
}

fn sub_to_ack(&(_, qos): &(TopicFilter, QualityOfService)) -> SubscribeReturnCode {
    match qos {
        QualityOfService::Level0 => SubscribeReturnCode::MaximumQoSLevel0,
        QualityOfService::Level1 => SubscribeReturnCode::MaximumQoSLevel1,
        QualityOfService::Level2 => SubscribeReturnCode::MaximumQoSLevel1,
    }
}

fn handle_client(mut stream: TcpStream, jsonpaths: Vec<String>, handle: Arc<Mutex<Write>>) {
    // This makes .read() call blocking.
    // Otherwise, mqtt decode consumes 100% CPU time.
    stream.set_read_timeout(None);

    log!("{:?}", stream);
    let auto_decode = VariablePacket::decode(&mut stream);
    log!("{:?}", auto_decode);
    ConnackPacket::new(false, ConnectReturnCode::ConnectionAccepted).encode(&mut stream);

    loop {
        let parse_result = VariablePacket::decode(&mut stream);
        if let Ok(packet) = parse_result {
            log!("{:?}", packet);
            match packet {
                VariablePacket::SubscribePacket(x) => {
                    SubackPacket::new(
                        x.packet_identifier(),
                        x.payload().subscribes().into_iter().map(sub_to_ack).collect()
                    ).encode(&mut stream);
                }
                VariablePacket::PingreqPacket(_) => {
                    PingrespPacket::new()
                        .encode(&mut stream);
                }
                VariablePacket::PublishPacket(x) => {
                    if let Ok(payload) = std::str::from_utf8(x.payload()) {
                        log!("{}: {}", x.topic_name(), payload);

                        let mut csv = jsonpaths.iter()
                            .map(|path| {
                                let res = lookup(String::from(payload), path.clone()).unwrap_or("".to_string());
                                if res != "[]" { res } else { "".to_string() }
                            })
                            .join(",");
                        csv.push('\n');

                        let mut handle = handle.lock().unwrap();
                        handle.write_all(csv.as_bytes());
                    }
                }
                _ => {
                }
            }
        }
    }
}
<|endoftext|>


<commit_before>//! A Takuzu (a.k.a. Binairo) solver.
//!
//! # Usage
//!
//! ```shell
//! tackle [FILE]...
//! tackle {--help | --version}
//! ```
//!
//! If no `FILE` is provided, or if `FILE` is '`-`', reads from standard input.

extern crate libc;
extern crate takuzu;

use std::fs::File;
use std::io::{stderr, stdin, Write};

use takuzu::{Grid, Source};

const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const USAGE_STRING: &'static str =
r#"Usage: tackle [FILE]...
       tackle {--help | --version}

If no FILE is provided, or if FILE is "-", read from standard input.

Options:
    --help       display this message and exit
    --version    display the version and exit
"#;

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    if args.contains(&"--help".to_owned()) {
        print!("{}", USAGE_STRING);
        return
    }
    if args.contains(&"--version".to_owned()) {
        println!("takle (takuzu) {}", VERSION);
        return
    }
    if args.len() == 0 { solve_from(&mut stdin()); }
    else {
        takle(&args[0], args.len() > 1);
        for filename in args.iter().skip(1) {
            print!("\n");
            takle(filename, true);
        }
    }
}

/// Opens a file (or stdin) and feeds
/// the source to the solving + printing function.
fn takle(filename: &String, print_filename: bool) {
    if filename == "-" {
        if print_filename { println!("-"); }
        solve_from(&mut stdin());
    }
    else {
        let mut file = match File::open(filename) {
            Ok(file) => file,
            Err(err) => {
                write!(stderr(), "\"{}\": {}\n", filename, err).unwrap();
                return
            }
        };
        if print_filename { println!("{}", filename); }
        solve_from(&mut file);
    }
}

/// Reads a grid from a source, triggers the solving algorithm
/// and prints the solutions.
pub fn solve_from(source: &mut Source) {
    let grid = match source.source() {
        Ok(grid) => grid,
        Err(err) => {
            write!(stderr(), "error: {}\n", err.0).unwrap();
            return
        },
    };
    let solutions = grid.solve();
    print_solutions(&grid, &solutions);
}

/// Prints the solution(s) to a grid on `stdout`.
///
/// Prints the grids with colors if appropriate (if `stdout` is a terminal).
/// If there is more than one solution, the grids are separated by
/// an empty line and preceded by a numbered label.
fn print_solutions(grid: &Grid, solutions: &Vec<Grid>) {
    if solutions.len() == 0 { write!(stderr(), "no solution (bug? I guess I owe you a cookie)\n").unwrap(); }
    else if solutions.len() == 1 {
        if isatty_stdout() { print!("{}", solutions[0].to_string_diff(&grid)); }
        else { print!("{}", solutions[0]); }
    }
    else {
        if isatty_stdout() {
            println!("solution 1");
            print!("{}", solutions[0].to_string_diff(&grid));
            for (i, sol) in solutions.into_iter().enumerate().skip(1) {
                println!("\nsolution {}", i + 1);
                print!("{}", sol.to_string_diff(&grid));
            }
        }
        else {
            print!("{}", solutions[0]);
            for sol in solutions.into_iter().skip(1) { print!("\n{}", sol); }
        }
    }
}

/// Returns `true` if `stdout` is a terminal.
pub fn isatty_stdout() -> bool {
    match unsafe { libc::isatty(libc::STDOUT_FILENO) } {
        1 => true,
        _ => false,
    }
}
<commit_msg>some grids having no solution is not a bug<commit_after>//! A Takuzu (a.k.a. Binairo) solver.
//!
//! # Usage
//!
//! ```shell
//! tackle [FILE]...
//! tackle {--help | --version}
//! ```
//!
//! If no `FILE` is provided, or if `FILE` is '`-`', reads from standard input.

extern crate libc;
extern crate takuzu;

use std::fs::File;
use std::io::{stderr, stdin, Write};

use takuzu::{Grid, Source};

const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const USAGE_STRING: &'static str =
r#"Usage: tackle [FILE]...
       tackle {--help | --version}

If no FILE is provided, or if FILE is "-", read from standard input.

Options:
    --help       display this message and exit
    --version    display the version and exit
"#;

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    if args.contains(&"--help".to_owned()) {
        print!("{}", USAGE_STRING);
        return
    }
    if args.contains(&"--version".to_owned()) {
        println!("takle (takuzu) {}", VERSION);
        return
    }
    if args.len() == 0 { solve_from(&mut stdin()); }
    else {
        takle(&args[0], args.len() > 1);
        for filename in args.iter().skip(1) {
            print!("\n");
            takle(filename, true);
        }
    }
}

/// Opens a file (or stdin) and feeds
/// the source to the solving + printing function.
fn takle(filename: &String, print_filename: bool) {
    if filename == "-" {
        if print_filename { println!("-"); }
        solve_from(&mut stdin());
    }
    else {
        let mut file = match File::open(filename) {
            Ok(file) => file,
            Err(err) => {
                write!(stderr(), "\"{}\": {}\n", filename, err).unwrap();
                return
            }
        };
        if print_filename { println!("{}", filename); }
        solve_from(&mut file);
    }
}

/// Reads a grid from a source, triggers the solving algorithm
/// and prints the solutions.
pub fn solve_from(source: &mut Source) {
    let grid = match source.source() {
        Ok(grid) => grid,
        Err(err) => {
            write!(stderr(), "error: {}\n", err.0).unwrap();
            return
        },
    };
    let solutions = grid.solve();
    print_solutions(&grid, &solutions);
}

/// Prints the solution(s) to a grid on `stdout`.
///
/// Prints the grids with colors if appropriate (if `stdout` is a terminal).
/// If there is more than one solution, the grids are separated by
/// an empty line and preceded by a numbered label.
fn print_solutions(grid: &Grid, solutions: &Vec<Grid>) {
    if solutions.len() == 0 { write!(stderr(), "no solution\n").unwrap(); }
    else if solutions.len() == 1 {
        if isatty_stdout() { print!("{}", solutions[0].to_string_diff(&grid)); }
        else { print!("{}", solutions[0]); }
    }
    else {
        if isatty_stdout() {
            println!("solution 1");
            print!("{}", solutions[0].to_string_diff(&grid));
            for (i, sol) in solutions.into_iter().enumerate().skip(1) {
                println!("\nsolution {}", i + 1);
                print!("{}", sol.to_string_diff(&grid));
            }
        }
        else {
            print!("{}", solutions[0]);
            for sol in solutions.into_iter().skip(1) { print!("\n{}", sol); }
        }
    }
}

/// Returns `true` if `stdout` is a terminal.
pub fn isatty_stdout() -> bool {
    match unsafe { libc::isatty(libc::STDOUT_FILENO) } {
        1 => true,
        _ => false,
    }
}
<|endoftext|>


<commit_before>extern crate ordered_vec_map;
extern crate termion;

use ordered_vec_map::OrderedVecMap;
use std::collections::HashMap;
use std::collections::VecDeque;
use termion::event::Key;

type KeyMap = OrderedVecMap<Vec<termion::event::Key>, Vec<termion::event::Key>>;

struct Mode<'a, T> {
    typeahead: &'a VecDeque<Key>,
    mode: T,
}

fn put_back(front: &Vec<Key>, typeahead: &mut VecDeque<Key>) {
    for key in front {
        typeahead.push_front(key.clone());
    }
}

struct NormalMode {}

struct InsertMode {}

struct Op {}

impl<'a> Mode<'a, NormalMode> {
    fn new(typeahead: &'a VecDeque<Key>) -> Self {
        Mode {
            typeahead: typeahead,
            mode: NormalMode {},
        }
    }

    fn echo(self, string: &str) {
        println!("{}", string)
    }
}

impl<'a> From<Mode<'a, InsertMode>> for Mode<'a, NormalMode> {
    fn from(current: Mode<'a, InsertMode>) -> Mode<'a, NormalMode> {
        Mode {
            typeahead: current.typeahead,
            mode: NormalMode {},
        }
    }
}

impl<'a> From<Mode<'a, NormalMode>> for Mode<'a, InsertMode> {
    fn from(current: Mode<'a, NormalMode>) -> Mode<'a, InsertMode> {
        Mode {
            typeahead: current.typeahead,
            mode: InsertMode {},
        }
    }
}

// fn execute(op_map: &HashMap<Vec<Key>, Op>,
//            front: &mut Vec<Key>) {
//     match op_map.get(front) {
//         Some(op) => { return true; } // TODO op(),
//         None => { return false; }
//     };
//     // Continue processing what's left in the buffer.
// }

enum Match<T> {
    FullMatch(T),
    PartialMatch(T),
    NoMatch,
}

fn find_match<'a>(map: &'a KeyMap, query: &Vec<Key>) -> Match<&'a Vec<Key>> {
    let partial_matcher = |probe: &(Vec<Key>, Vec<Key>)| if probe.0.len() >
        query.len() &&
        probe.0.starts_with(query)
    {
        return std::cmp::Ordering::Equal;
    } else {
        return probe.0.cmp(query);
    };

    // Check for any partial matches against the entire input, where all input
    // keys match the first N map keys.
    map.find_by(partial_matcher).map_or(
        // Then, check for full matches. If any are found, return the longest
        // full match, where all map keys match the first N input keys.
        // Otherwise, return no match.
        map.find(query).map_or(Match::NoMatch, |val| {
            Match::FullMatch(val)
        }),
        |val| Match::PartialMatch(val),
    )
}

// fn remap(
//     map: &KeyMap,
//     front: &Vec<Key>,
//     typeahead: &mut VecDeque<Key>,
// ) -> bool {
//     match find_match(map, front) {
//         Match::FullMatch(mapped_keys) => {
//             // Clone mapped keys in front of the typeahead buffer.
//             for key in mapped_keys {
//                 typeahead.push_front(key.clone());
//             }
//             return true;
//         }
//         Match::PartialMatch(_) => {
//             return false;
//         }
//         Match::NoMatch => {
//             return false;
//         }
//     };
// }

struct ModeMap {
    key_remap: KeyMap,
    key_noremap: KeyMap,
    op_map: HashMap<Vec<Key>, Op>,
}

impl ModeMap {
    // Loop until a partly matching mapping is found or all (local) mappings
    // have been checked.  The longest full match is remembered in "mp_match".
    // A full match is only accepted if there is no partly match, so "aa" and
    // "aaa" can both be mapped.
    // https://github.com/vim/vim/blob/master/src/getchar.c#L2140-L2146
    fn process(self, typeahead: &mut VecDeque<Key>) {
        // Grab incrementally more keys from the front of the queue, looking for
        // matches.
        let mut front = Vec::<Key>::with_capacity(typeahead.len());
        while !typeahead.is_empty() {
            front.push(typeahead.pop_front().unwrap());
            match find_match(&self.key_remap, &front) {
                Match::FullMatch(mapped_keys) => {
                    // Put mapped keys in front of the typeahead buffer.
                    put_back(mapped_keys, typeahead);
                    // Clear front (ergo, we'll skip matching noremap and op
                    // until next iteration).
                    front.clear();
                }
                Match::PartialMatch(_) => {
                    // Keep searching.
                }
                Match::NoMatch => {
                    // We're done searching.
                    return;
                }
            };
        }
        // Put whatever is left back in the typeahead buffer.
        put_back(&mut front, typeahead);
    }
}

fn main() {
    let mut typeahead = VecDeque::<Key>::new();
    typeahead.push_back(Key::Char('i'));
    typeahead.push_back(Key::Esc);

    let normal_mode = Mode::<NormalMode>::new(&typeahead);
    let insert_mode = Mode::<InsertMode>::from(normal_mode);
    let normal_mode = Mode::<NormalMode>::from(insert_mode);
    normal_mode.echo("asdf");
}
<commit_msg>determine a noremap strategy<commit_after>extern crate ordered_vec_map;
extern crate termion;

use ordered_vec_map::OrderedVecMap;
use std::collections::HashMap;
use std::collections::VecDeque;
use termion::event::Key;

type KeyMap<T> = OrderedVecMap<Vec<termion::event::Key>, T>;
type KeyRemap = KeyMap<Vec<termion::event::Key>>;

struct Mode<'a, T> {
    typeahead: &'a VecDeque<Key>,
    mode: T,
}

fn put_back(front: &Vec<Key>, typeahead: &mut VecDeque<Key>) {
    for key in front {
        typeahead.push_front(key.clone());
    }
}

struct NormalMode {}

struct InsertMode {}

struct Op {}

impl<'a> Mode<'a, NormalMode> {
    fn new(typeahead: &'a VecDeque<Key>) -> Self {
        Mode {
            typeahead: typeahead,
            mode: NormalMode {},
        }
    }

    fn echo(self, string: &str) {
        println!("{}", string)
    }
}

impl<'a> From<Mode<'a, InsertMode>> for Mode<'a, NormalMode> {
    fn from(current: Mode<'a, InsertMode>) -> Mode<'a, NormalMode> {
        Mode {
            typeahead: current.typeahead,
            mode: NormalMode {},
        }
    }
}

impl<'a> From<Mode<'a, NormalMode>> for Mode<'a, InsertMode> {
    fn from(current: Mode<'a, NormalMode>) -> Mode<'a, InsertMode> {
        Mode {
            typeahead: current.typeahead,
            mode: InsertMode {},
        }
    }
}

enum Match<T> {
    FullMatch(T),
    PartialMatch(T),
    NoMatch,
}

fn find_match<'a, T>(map: &'a KeyMap<T>, query: &Vec<Key>) -> Match<&'a T> {
    let partial_matcher = |probe: &(Vec<Key>, T)| if probe.0.len() >
        query.len() &&
        probe.0.starts_with(query)
    {
        return std::cmp::Ordering::Equal;
    } else {
        return probe.0.cmp(query);
    };

    // Check for any partial matches against the entire input, where all input
    // keys match the first N map keys.
    map.find_by(partial_matcher).map_or(
        // Then, check for full matches. If any are found, return the longest
        // full match, where all map keys match the first N input keys.
        // Otherwise, return no match.
        map.find(query).map_or(Match::NoMatch, |val| {
            Match::FullMatch(val)
        }),
        |val| Match::PartialMatch(val),
    )
}

struct ModeMap {
    // TODO Handle noremap (key,value) by surrounding value with non-input-able
    // keys, so if it gets put in the typeahead, it cannot possibly be remapped.
    // This would also mean such values would be ignored by the op-map.
    key_remap: KeyRemap,
    op_map: KeyMap<Op>,
}

impl ModeMap {
    // Loop until a partly matching mapping is found or all (local) mappings
    // have been checked.  The longest full match is remembered in "mp_match".
    // A full match is only accepted if there is no partly match, so "aa" and
    // "aaa" can both be mapped.
    // https://github.com/vim/vim/blob/master/src/getchar.c#L2140-L2146
    fn process(self, typeahead: &mut VecDeque<Key>) {
        // Grab incrementally more keys from the front of the queue, looking for
        // matches.
        let mut front = Vec::<Key>::with_capacity(typeahead.len());
        while !typeahead.is_empty() {
            front.push(typeahead.pop_front().unwrap());
            match find_match(&self.key_remap, &front) {
                Match::FullMatch(mapped_keys) => {
                    // Put mapped keys in front of the typeahead buffer.
                    put_back(mapped_keys, typeahead);
                    // Clear front (ergo, we'll skip matching noremap and op
                    // until next iteration).
                    front.clear();
                }
                Match::PartialMatch(_) => {
                    // Keep searching.
                }
                Match::NoMatch => {
                    // We're done searching.
                    return;
                }
            };
            match find_match(&self.op_map, &front) {
                Match::FullMatch(op) => {
                    // TODO Apply op.
                    // op()

                    // Clear front (ergo, we'll skip matching noremap and op
                    // until next iteration).
                    front.clear();
                }
                Match::PartialMatch(_) => {
                    // Keep searching.
                }
                Match::NoMatch => {
                    // We're done searching.
                    return;
                }
            };
        }
        // Put whatever is left back in the typeahead buffer.
        put_back(&front, typeahead);
    }
}

fn main() {
    let mut typeahead = VecDeque::<Key>::new();
    typeahead.push_back(Key::Char('i'));
    typeahead.push_back(Key::Esc);

    let normal_mode = Mode::<NormalMode>::new(&typeahead);
    let insert_mode = Mode::<InsertMode>::from(normal_mode);
    let normal_mode = Mode::<NormalMode>::from(insert_mode);
    normal_mode.echo("asdf");
}
<|endoftext|>