<commit_before>extern crate clap;
extern crate termion;

use std::collections::HashMap;
use std::cmp::min;
use std::fs::File;
use std::io::{stdin, stdout, BufRead, BufReader, Write};
use std::option::Option;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread;
use std::vec::Vec;

use clap::{App, Arg};
use termion::color;
use termion::event::{Event, Key, MouseEvent};
use termion::input::{MouseTerminal, TermRead};
use termion::raw::IntoRawMode;

const VERT_SEPARATOR: &'static str = "│";
const LIST_WIDTH: u16 = 32;

enum ChanMessage {
    SearchResult(ListEntry),
    SearchComplete,
    TerminalEvent(Event),
}

#[derive(Debug, Clone)]
struct ListEntry {
    directory: String,
    file_name: String,
    line_number: u32,
    text: String,
}

impl ListEntry {
    // format is: path/to/file.rs:123:line of text
    fn from_line(line: &str) -> Option<ListEntry> {
        let parts: Vec<&str> = line.splitn(3, ':').collect();

        if parts.len() != 3 {
            return None;
        }

        let path = Path::new(parts[0]);

        // FIXME: This cannot possibly be right. wtf.
        Some(ListEntry {
            directory: String::from(path.parent().unwrap().to_str().unwrap()),
            file_name: String::from(path.file_name().unwrap().to_str().unwrap()),
            line_number: parts[1].parse().unwrap(),
            text: String::from(parts[2]),
        })
    }
}

#[derive(Debug, Clone)]
struct FileList {
    entries: Vec<ListEntry>,
    current_index: Option<usize>,
}

impl FileList {
    fn new() -> FileList {
        FileList {
            entries: vec![],
            current_index: None,
        }
    }

    fn add_entry(&mut self, entry: ListEntry) {
        // Insert the new element into it's sorted position
        let insert_pos = {
            let new_key = (&entry.directory, &entry.file_name, &entry.line_number);
            let result = self.entries.binary_search_by(|other| {
                let k = (&other.directory, &other.file_name, &other.line_number);
                return k.cmp(&new_key);
            });

            match result {
                Ok(pos) | Err(pos) => pos,
            }
        };

        self.entries.insert(insert_pos, entry);

        if self.current_index.is_none() {
            self.current_index = Some(0);
        }
    }

    fn scroll_down(&mut self) {
        self.current_index = self.current_index
            .map(|prev| min(self.entries.len() - 1, prev + 1))
    }

    fn scroll_up(&mut self) {
        self.current_index = self.current_index
            .map(|prev| if prev == 0 { prev } else { prev - 1 })
    }
}

#[derive(Debug)]
struct UI {
    list: FileList,
    file_cache: HashMap<(String, String), File>,
}

fn main() {
    let matches = App::new("dredge")
        .about("Search interactively")
        .arg(
            Arg::with_name("program")
                .long("program")
                .short("p")
                .value_name("PROGRAM")
                .default_value("ag")
                .help("grep or ag or w/e"),
        )
        .arg(
            Arg::with_name("query")
                .help("what to search for")
                .required(true),
        )
        // .arg(
        //     Arg::with_name("directories")
        //         .help("where to search for it")
        //         .multiple(true)
        //         .default_value("."),
        // )
        // .arg(
        //     Arg::with_name("options")
        //         .help("options to pass directly to search program.")
        //         .multiple(true)
        //         .default_value("")
        //         .last(true),
        // )
        .get_matches();

    let (tx, rx) = channel();

    let cmd_tx = tx.clone();
    let term_tx = tx.clone();

    thread::spawn(move || {
        run_command(matches, cmd_tx).expect("failed to run command");
    });

    thread::spawn(move || {
        let stdin = stdin();

        for c in stdin.events() {
            term_tx
                .send(ChanMessage::TerminalEvent(c.unwrap()))
                .unwrap();
        }
    });

    ui_loop(rx).unwrap();
}

fn run_command(
    config: clap::ArgMatches,
    sender: Sender<ChanMessage>,
) -> Result<(), Box<std::error::Error>> {
    let cmd = Command::new("ag")
        .arg(config.value_of("query").unwrap())
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .spawn()?;

    let output = cmd.stdout.unwrap();
    let reader = BufReader::new(output);

    for line in reader.lines() {
        if let Some(entry) = ListEntry::from_line(&line.unwrap()) {
            sender.send(ChanMessage::SearchResult(entry))?;
        }
    }

    sender.send(ChanMessage::SearchComplete)?;

    Ok(())
}

fn ui_loop(receiver: Receiver<ChanMessage>) -> Result<(), Box<std::error::Error>> {
    let mut file_list = FileList::new();

    let mut stdout = MouseTerminal::from(stdout().into_raw_mode()?);
    write!(
        stdout,
        "{}{}",
        termion::clear::All,
        termion::cursor::Goto(1, 1)
    )?;

    for entry in receiver.iter() {
        let mut redraw = false;

        match entry {
            ChanMessage::SearchComplete => {
                if file_list.entries.is_empty() {
                    println!("No matches");
                    break;
                }
            }

            ChanMessage::TerminalEvent(evt) => match evt {
                Event::Key(Key::Ctrl('c')) | Event::Key(Key::Char('q')) => break,

                Event::Key(Key::Char('j')) => {
                    redraw = true;
                    file_list.scroll_down();
                }

                Event::Key(Key::Char('k')) => {
                    redraw = true;
                    file_list.scroll_up();
                }

                Event::Mouse(me) => match me {
                    MouseEvent::Press(_, x, y) => {
                        write!(stdout, "{}x", termion::cursor::Goto(x, y))?;
                        stdout.flush()?;
                    }

                    _ => (),
                },

                _ => {}
            },

            ChanMessage::SearchResult(entry) => {
                redraw = true;
                file_list.add_entry(entry);

                if file_list.current_index.is_none() {
                    file_list.current_index = Some(0)
                }
            }
        }

        if redraw {
            let (width, height) = termion::terminal_size()?;
            let offset = file_list.current_index.unwrap_or(0);

            for row in 0..height {
                let index = (offset + row as usize) as usize;

                if index < file_list.entries.len() {
                    let ref entry = &file_list.entries[index];
                    let (fg, bg): (&color::Color, &color::Color) =
                        if Some(index) == file_list.current_index {
                            (&color::Black, &color::White)
                        } else {
                            (&color::Reset, &color::Reset)
                        };

                    write!(
                        stdout,
                        "{}{}{}{}/{}:{}{}{}",
                        termion::cursor::Goto(1, 1 + row as u16),
                        color::Fg(fg),
                        color::Bg(bg),
                        entry.directory,
                        entry.file_name,
                        entry.line_number,
                        color::Fg(color::Reset),
                        color::Bg(color::Reset),
                    )?;
                } else {
                    write!(
                        stdout,
                        "{}{}",
                        termion::cursor::Goto(1, 1 + row as u16),
                        "                     "
                    )?;
                }

                write!(
                    stdout,
                    "{}{}",
                    termion::cursor::Goto(LIST_WIDTH, 1 + row),
                    VERT_SEPARATOR
                )?;
            }

            write!(stdout, "{}", termion::cursor::Hide)?;
            stdout.flush()?;
        }
    }

    Ok(())
}
<commit_msg>Implement better scrolling<commit_after>extern crate clap;
extern crate termion;

use std::collections::HashMap;
use std::cmp::{max, min};
use std::fs::File;
use std::io::{stdin, stdout, BufRead, BufReader, Write};
use std::option::Option;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::vec::Vec;

use std::thread;

use clap::{App, Arg};
use termion::color;
use termion::event::{Event, Key, MouseEvent};
use termion::input::{MouseTerminal, TermRead};
use termion::raw::IntoRawMode;

const VERT_SEPARATOR: &'static str = "│";
const LIST_WIDTH: u16 = 32;

// FIXME: Support handling SIGWINCH for rerender on resize.
enum ChanMessage {
    SearchResult(ListEntry),
    SearchComplete,
    TerminalEvent(Event),
}

#[derive(Debug, Clone)]
struct ListEntry {
    directory: String,
    file_name: String,
    line_number: u32,
    text: String,
}

impl ListEntry {
    // format is: path/to/file.rs:123:line of text
    fn from_line(line: &str) -> Option<ListEntry> {
        let parts: Vec<&str> = line.splitn(3, ':').collect();

        if parts.len() != 3 {
            return None;
        }

        let path = Path::new(parts[0]);

        // FIXME: This cannot possibly be right. wtf.
        Some(ListEntry {
            directory: String::from(path.parent().unwrap().to_str().unwrap()),
            file_name: String::from(path.file_name().unwrap().to_str().unwrap()),
            line_number: parts[1].parse().unwrap(),
            text: String::from(parts[2]),
        })
    }
}

#[derive(Debug, Clone)]
struct FileList {
    entries: Vec<ListEntry>,
    current_index: Option<usize>,
}

impl FileList {
    fn new() -> FileList {
        FileList {
            entries: vec![],
            current_index: None,
        }
    }

    fn add_entry(&mut self, entry: ListEntry) {
        // Insert the new element into it's sorted position
        let insert_pos = {
            let new_key = (&entry.directory, &entry.file_name, &entry.line_number);
            let result = self.entries.binary_search_by(|other| {
                let k = (&other.directory, &other.file_name, &other.line_number);
                return k.cmp(&new_key);
            });

            match result {
                Ok(pos) | Err(pos) => pos,
            }
        };

        self.entries.insert(insert_pos, entry);

        if self.current_index.is_none() {
            self.current_index = Some(0);
        }
    }

    fn scroll_down(&mut self) {
        self.current_index = self.current_index
            .map(|prev| min(self.entries.len() - 1, prev + 1))
    }

    fn scroll_up(&mut self) {
        self.current_index = self.current_index
            .map(|prev| if prev == 0 { prev } else { prev - 1 })
    }
}

#[derive(Debug)]
struct UI {
    list: FileList,
    file_cache: HashMap<(String, String), File>,
}

fn main() {
    let matches = App::new("dredge")
        .about("Search interactively")
        .arg(
            Arg::with_name("program")
                .long("program")
                .short("p")
                .value_name("PROGRAM")
                .default_value("ag")
                .help("grep or ag or w/e"),
        )
        .arg(
            Arg::with_name("query")
                .help("what to search for")
                .required(true),
        )
        // .arg(
        //     Arg::with_name("directories")
        //         .help("where to search for it")
        //         .multiple(true)
        //         .default_value("."),
        // )
        // .arg(
        //     Arg::with_name("options")
        //         .help("options to pass directly to search program.")
        //         .multiple(true)
        //         .default_value("")
        //         .last(true),
        // )
        .get_matches();

    let (tx, rx) = channel();

    let cmd_tx = tx.clone();
    let term_tx = tx.clone();

    thread::spawn(move || {
        run_command(matches, cmd_tx).expect("failed to run command");
    });

    thread::spawn(move || {
        let stdin = stdin();

        for c in stdin.events() {
            term_tx
                .send(ChanMessage::TerminalEvent(c.unwrap()))
                .unwrap();
        }
    });

    ui_loop(rx).unwrap();
}

fn run_command(
    config: clap::ArgMatches,
    sender: Sender<ChanMessage>,
) -> Result<(), Box<std::error::Error>> {
    let cmd = Command::new("ag")
        .arg(config.value_of("query").unwrap())
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .spawn()?;

    let output = cmd.stdout.unwrap();
    let reader = BufReader::new(output);

    for line in reader.lines() {
        if let Some(entry) = ListEntry::from_line(&line.unwrap()) {
            sender.send(ChanMessage::SearchResult(entry))?;
        }
    }

    sender.send(ChanMessage::SearchComplete)?;

    Ok(())
}

fn ui_loop(receiver: Receiver<ChanMessage>) -> Result<(), Box<std::error::Error>> {
    let mut file_list = FileList::new();

    let mut stdout = MouseTerminal::from(stdout().into_raw_mode()?);
    write!(
        stdout,
        "{}{}",
        termion::clear::All,
        termion::cursor::Goto(1, 1)
    )?;

    let mut min_index = 0;

    for entry in receiver.iter() {
        let (_width, height) = termion::terminal_size()?;
        let mut redraw = true;

        match entry {
            ChanMessage::SearchComplete => {
                if file_list.entries.is_empty() {
                    println!("No matches");
                    break;
                }
            }

            ChanMessage::TerminalEvent(evt) => match evt {
                Event::Key(Key::Ctrl('c')) | Event::Key(Key::Char('q')) => break,

                Event::Key(Key::Char('j')) => {
                    file_list.scroll_down();
                }

                Event::Key(Key::Char('k')) => {
                    file_list.scroll_up();
                }

                Event::Mouse(me) => match me {
                    MouseEvent::Press(_, x, y) => {
                        write!(stdout, "{}x", termion::cursor::Goto(x, y))?;
                        stdout.flush()?;
                    }

                    _ => (),
                },

                _ => {
                    redraw = false;
                }
            },

            ChanMessage::SearchResult(entry) => {
                redraw = true;
                file_list.add_entry(entry);

                if file_list.current_index.is_none() {
                    file_list.current_index = Some(0)
                }
            }
        }

        if redraw {
            min_index = file_list.current_index.map_or(0, |cur| {
                if cur < min_index {
                    min_index - 1
                } else if cur >= min_index + height as usize {
                    cur
                } else {
                    min_index
                }
            });

            for row in 0..height {
                write!(stdout, "{}", termion::cursor::Goto(1, 1 + row as u16))?;

                let index = (min_index + row as usize) as usize;

                if index < file_list.entries.len() {
                    let ref entry = &file_list.entries[index];
                    let (fg, bg): (&color::Color, &color::Color) =
                        if Some(index) == file_list.current_index {
                            (&color::Black, &color::White)
                        } else {
                            (&color::Reset, &color::Reset)
                        };

                    let entry_fmt = format!(
                        "{}/{}:{}",
                        entry.directory, entry.file_name, entry.line_number
                    );

                    write!(
                        stdout,
                        "{}{}{:<32}{}{}",
                        color::Fg(fg),
                        color::Bg(bg),
                        entry_fmt,
                        color::Fg(color::Reset),
                        color::Bg(color::Reset),
                    )?;
                } else {
                    write!(stdout, "{:<32}", "")?;
                }

                write!(
                    stdout,
                    "{}{}",
                    termion::cursor::Goto(LIST_WIDTH, 1 + row),
                    VERT_SEPARATOR
                )?;
            }

            write!(stdout, "{}", termion::cursor::Hide)?;
            stdout.flush()?;
        }
    }

    Ok(())
}
<|endoftext|>


<commit_before>extern crate clap;
extern crate termion;

use std::collections::HashMap;
use std::cmp::{max, min};
use std::fs::File;
use std::io::{stdin, stdout, BufRead, BufReader, Write};
use std::option::Option;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::vec::Vec;

use std::thread;

use clap::{App, Arg};
use termion::color;
use termion::event::{Event, Key, MouseEvent};
use termion::input::{MouseTerminal, TermRead};
use termion::screen::AlternateScreen;
use termion::raw::IntoRawMode;

const VERT_SEPARATOR: &'static str = "│";
const LIST_WIDTH: u16 = 32;

// FIXME: Support handling SIGWINCH for rerender on resize.
enum ChanMessage {
    SearchResult(ListEntry),
    SearchComplete,
    TerminalEvent(Event),
}

#[derive(Debug, Clone)]
struct ListEntry {
    directory: String,
    file_name: String,
    line_number: u32,
    text: String,
}

impl ListEntry {
    // format is: path/to/file.rs:123:line of text
    fn from_line(line: &str) -> Option<ListEntry> {
        let parts: Vec<&str> = line.splitn(3, ':').collect();

        if parts.len() != 3 {
            return None;
        }

        let path = Path::new(parts[0]);

        // FIXME: This cannot possibly be right. wtf.
        Some(ListEntry {
            directory: String::from(path.parent().unwrap().to_str().unwrap()),
            file_name: String::from(path.file_name().unwrap().to_str().unwrap()),
            line_number: parts[1].parse().unwrap(),
            text: String::from(parts[2]),
        })
    }
}

#[derive(Debug, Clone)]
struct FileList {
    entries: Vec<ListEntry>,
    current_index: Option<usize>,
}

impl FileList {
    fn new() -> FileList {
        FileList {
            entries: vec![],
            current_index: None,
        }
    }

    fn add_entry(&mut self, entry: ListEntry) {
        // Insert the new element into it's sorted position
        let insert_pos = {
            let new_key = (&entry.directory, &entry.file_name, &entry.line_number);
            let result = self.entries.binary_search_by(|other| {
                let k = (&other.directory, &other.file_name, &other.line_number);
                return k.cmp(&new_key);
            });

            match result {
                Ok(pos) | Err(pos) => pos,
            }
        };

        self.entries.insert(insert_pos, entry);

        if self.current_index.is_none() {
            self.current_index = Some(0);
        }
    }

    fn scroll_down(&mut self) {
        self.current_index = self.current_index
            .map(|prev| min(self.entries.len() - 1, prev + 1))
    }

    fn scroll_up(&mut self) {
        self.current_index = self.current_index
            .map(|prev| if prev == 0 { prev } else { prev - 1 })
    }
}

#[derive(Debug)]
struct UI {
    list: FileList,
    file_cache: HashMap<(String, String), File>,
}

fn main() {
    let matches = App::new("dredge")
        .about("Search interactively")
        .arg(
            Arg::with_name("program")
                .long("program")
                .short("p")
                .value_name("PROGRAM")
                .default_value("ag")
                .help("grep or ag or w/e"),
        )
        .arg(
            Arg::with_name("query")
                .help("what to search for")
                .required(true),
        )
        // .arg(
        //     Arg::with_name("directories")
        //         .help("where to search for it")
        //         .multiple(true)
        //         .default_value("."),
        // )
        // .arg(
        //     Arg::with_name("options")
        //         .help("options to pass directly to search program.")
        //         .multiple(true)
        //         .default_value("")
        //         .last(true),
        // )
        .get_matches();

    let (tx, rx) = channel();

    let cmd_tx = tx.clone();
    let term_tx = tx.clone();

    thread::spawn(move || {
        run_command(matches, cmd_tx).expect("failed to run command");
    });

    thread::spawn(move || {
        let stdin = stdin();

        for c in stdin.events() {
            term_tx
                .send(ChanMessage::TerminalEvent(c.unwrap()))
                .unwrap();
        }
    });

    ui_loop(rx).unwrap();
}

fn run_command(
    config: clap::ArgMatches,
    sender: Sender<ChanMessage>,
) -> Result<(), Box<std::error::Error>> {
    let cmd = Command::new("ag")
        .arg(config.value_of("query").unwrap())
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .spawn()?;

    let output = cmd.stdout.unwrap();
    let reader = BufReader::new(output);

    for line in reader.lines() {
        if let Some(entry) = ListEntry::from_line(&line.unwrap()) {
            sender.send(ChanMessage::SearchResult(entry))?;
        }
    }

    sender.send(ChanMessage::SearchComplete)?;

    Ok(())
}

fn ui_loop(receiver: Receiver<ChanMessage>) -> Result<(), Box<std::error::Error>> {
    let mut file_list = FileList::new();

    let screen = AlternateScreen::from(stdout());
    let mut stdout = MouseTerminal::from(screen.into_raw_mode()?);
    write!(
        stdout,
        "{}{}",
        termion::clear::All,
        termion::cursor::Goto(1, 1)
    )?;

    let mut min_index = 0;

    for entry in receiver.iter() {
        let (_width, height) = termion::terminal_size()?;
        let mut redraw = true;

        match entry {
            ChanMessage::SearchComplete => {
                if file_list.entries.is_empty() {
                    println!("No matches");
                    break;
                }
            }

            ChanMessage::TerminalEvent(evt) => match evt {
                Event::Key(Key::Ctrl('c')) | Event::Key(Key::Char('q')) => break,

                Event::Key(Key::Char('j')) => file_list.scroll_down(),
                Event::Key(Key::Char('k')) => file_list.scroll_up(),

                Event::Mouse(me) => match me {
                    MouseEvent::Press(_, x, y) => {
                        if x < LIST_WIDTH {
                            // TODO: have standards.
                            file_list.current_index = file_list.current_index.map(|_| {
                                max(1, min(file_list.entries.len(), min_index + y as usize)) - 1
                            });
                        }
                    }

                    _ => (),
                },

                _ => {
                    redraw = false;
                }
            },

            ChanMessage::SearchResult(entry) => {
                redraw = true;
                file_list.add_entry(entry);

                if file_list.current_index.is_none() {
                    file_list.current_index = Some(0)
                }
            }
        }

        if redraw {
            min_index = file_list.current_index.map_or(0, |cur| {
                if cur < min_index {
                    min_index - 1
                } else if cur >= min_index + height as usize {
                    cur
                } else {
                    min_index
                }
            });

            for row in 0..height {
                write!(stdout, "{}", termion::cursor::Goto(1, 1 + row as u16))?;

                let index = (min_index + row as usize) as usize;

                if index < file_list.entries.len() {
                    let ref entry = &file_list.entries[index];
                    let (fg, bg): (&color::Color, &color::Color) =
                        if Some(index) == file_list.current_index {
                            (&color::Black, &color::White)
                        } else {
                            (&color::Reset, &color::Reset)
                        };

                    let entry_fmt = format!(
                        "{}/{}:{}",
                        entry.directory, entry.file_name, entry.line_number
                    );

                    write!(
                        stdout,
                        "{}{}{:<32}{}{}",
                        color::Fg(fg),
                        color::Bg(bg),
                        entry_fmt,
                        color::Fg(color::Reset),
                        color::Bg(color::Reset),
                    )?;
                } else {
                    write!(stdout, "{:<32}", "")?;
                }

                write!(
                    stdout,
                    "{}{}",
                    termion::cursor::Goto(LIST_WIDTH, 1 + row),
                    VERT_SEPARATOR
                )?;
            }

            write!(stdout, "{}", termion::cursor::Hide)?;
            stdout.flush()?;
        }
    }

    write!(
        stdout,
        "{}{}",
        termion::clear::All,
        termion::cursor::Show
    )?;
    stdout.flush()?;

    Ok(())
}
<commit_msg>cargo fmt<commit_after>extern crate clap;
extern crate termion;

use std::collections::HashMap;
use std::cmp::{max, min};
use std::fs::File;
use std::io::{stdin, stdout, BufRead, BufReader, Write};
use std::option::Option;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::vec::Vec;

use std::thread;

use clap::{App, Arg};
use termion::color;
use termion::event::{Event, Key, MouseEvent};
use termion::input::{MouseTerminal, TermRead};
use termion::screen::AlternateScreen;
use termion::raw::IntoRawMode;

const VERT_SEPARATOR: &'static str = "│";
const LIST_WIDTH: u16 = 32;

// FIXME: Support handling SIGWINCH for rerender on resize.
enum ChanMessage {
    SearchResult(ListEntry),
    SearchComplete,
    TerminalEvent(Event),
}

#[derive(Debug, Clone)]
struct ListEntry {
    directory: String,
    file_name: String,
    line_number: u32,
    text: String,
}

impl ListEntry {
    // format is: path/to/file.rs:123:line of text
    fn from_line(line: &str) -> Option<ListEntry> {
        let parts: Vec<&str> = line.splitn(3, ':').collect();

        if parts.len() != 3 {
            return None;
        }

        let path = Path::new(parts[0]);

        // FIXME: This cannot possibly be right. wtf.
        Some(ListEntry {
            directory: String::from(path.parent().unwrap().to_str().unwrap()),
            file_name: String::from(path.file_name().unwrap().to_str().unwrap()),
            line_number: parts[1].parse().unwrap(),
            text: String::from(parts[2]),
        })
    }
}

#[derive(Debug, Clone)]
struct FileList {
    entries: Vec<ListEntry>,
    current_index: Option<usize>,
}

impl FileList {
    fn new() -> FileList {
        FileList {
            entries: vec![],
            current_index: None,
        }
    }

    fn add_entry(&mut self, entry: ListEntry) {
        // Insert the new element into it's sorted position
        let insert_pos = {
            let new_key = (&entry.directory, &entry.file_name, &entry.line_number);
            let result = self.entries.binary_search_by(|other| {
                let k = (&other.directory, &other.file_name, &other.line_number);
                return k.cmp(&new_key);
            });

            match result {
                Ok(pos) | Err(pos) => pos,
            }
        };

        self.entries.insert(insert_pos, entry);

        if self.current_index.is_none() {
            self.current_index = Some(0);
        }
    }

    fn scroll_down(&mut self) {
        self.current_index = self.current_index
            .map(|prev| min(self.entries.len() - 1, prev + 1))
    }

    fn scroll_up(&mut self) {
        self.current_index = self.current_index
            .map(|prev| if prev == 0 { prev } else { prev - 1 })
    }
}

#[derive(Debug)]
struct UI {
    list: FileList,
    file_cache: HashMap<(String, String), File>,
}

fn main() {
    let matches = App::new("dredge")
        .about("Search interactively")
        .arg(
            Arg::with_name("program")
                .long("program")
                .short("p")
                .value_name("PROGRAM")
                .default_value("ag")
                .help("grep or ag or w/e"),
        )
        .arg(
            Arg::with_name("query")
                .help("what to search for")
                .required(true),
        )
        // .arg(
        //     Arg::with_name("directories")
        //         .help("where to search for it")
        //         .multiple(true)
        //         .default_value("."),
        // )
        // .arg(
        //     Arg::with_name("options")
        //         .help("options to pass directly to search program.")
        //         .multiple(true)
        //         .default_value("")
        //         .last(true),
        // )
        .get_matches();

    let (tx, rx) = channel();

    let cmd_tx = tx.clone();
    let term_tx = tx.clone();

    thread::spawn(move || {
        run_command(matches, cmd_tx).expect("failed to run command");
    });

    thread::spawn(move || {
        let stdin = stdin();

        for c in stdin.events() {
            term_tx
                .send(ChanMessage::TerminalEvent(c.unwrap()))
                .unwrap();
        }
    });

    ui_loop(rx).unwrap();
}

fn run_command(
    config: clap::ArgMatches,
    sender: Sender<ChanMessage>,
) -> Result<(), Box<std::error::Error>> {
    let cmd = Command::new("ag")
        .arg(config.value_of("query").unwrap())
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .spawn()?;

    let output = cmd.stdout.unwrap();
    let reader = BufReader::new(output);

    for line in reader.lines() {
        if let Some(entry) = ListEntry::from_line(&line.unwrap()) {
            sender.send(ChanMessage::SearchResult(entry))?;
        }
    }

    sender.send(ChanMessage::SearchComplete)?;

    Ok(())
}

fn ui_loop(receiver: Receiver<ChanMessage>) -> Result<(), Box<std::error::Error>> {
    let mut file_list = FileList::new();

    let screen = AlternateScreen::from(stdout());
    let mut stdout = MouseTerminal::from(screen.into_raw_mode()?);
    write!(
        stdout,
        "{}{}",
        termion::clear::All,
        termion::cursor::Goto(1, 1)
    )?;

    let mut min_index = 0;

    for entry in receiver.iter() {
        let (_width, height) = termion::terminal_size()?;
        let mut redraw = true;

        match entry {
            ChanMessage::SearchComplete => {
                if file_list.entries.is_empty() {
                    println!("No matches");
                    break;
                }
            }

            ChanMessage::TerminalEvent(evt) => match evt {
                Event::Key(Key::Ctrl('c')) | Event::Key(Key::Char('q')) => break,

                Event::Key(Key::Char('j')) => file_list.scroll_down(),
                Event::Key(Key::Char('k')) => file_list.scroll_up(),

                Event::Mouse(me) => match me {
                    MouseEvent::Press(_, x, y) => {
                        if x < LIST_WIDTH {
                            // TODO: have standards.
                            file_list.current_index = file_list.current_index.map(|_| {
                                max(1, min(file_list.entries.len(), min_index + y as usize)) - 1
                            });
                        }
                    }

                    _ => (),
                },

                _ => {
                    redraw = false;
                }
            },

            ChanMessage::SearchResult(entry) => {
                redraw = true;
                file_list.add_entry(entry);

                if file_list.current_index.is_none() {
                    file_list.current_index = Some(0)
                }
            }
        }

        if redraw {
            min_index = file_list.current_index.map_or(0, |cur| {
                if cur < min_index {
                    min_index - 1
                } else if cur >= min_index + height as usize {
                    cur
                } else {
                    min_index
                }
            });

            for row in 0..height {
                write!(stdout, "{}", termion::cursor::Goto(1, 1 + row as u16))?;

                let index = (min_index + row as usize) as usize;

                if index < file_list.entries.len() {
                    let ref entry = &file_list.entries[index];
                    let (fg, bg): (&color::Color, &color::Color) =
                        if Some(index) == file_list.current_index {
                            (&color::Black, &color::White)
                        } else {
                            (&color::Reset, &color::Reset)
                        };

                    let entry_fmt = format!(
                        "{}/{}:{}",
                        entry.directory, entry.file_name, entry.line_number
                    );

                    write!(
                        stdout,
                        "{}{}{:<32}{}{}",
                        color::Fg(fg),
                        color::Bg(bg),
                        entry_fmt,
                        color::Fg(color::Reset),
                        color::Bg(color::Reset),
                    )?;
                } else {
                    write!(stdout, "{:<32}", "")?;
                }

                write!(
                    stdout,
                    "{}{}",
                    termion::cursor::Goto(LIST_WIDTH, 1 + row),
                    VERT_SEPARATOR
                )?;
            }

            write!(stdout, "{}", termion::cursor::Hide)?;
            stdout.flush()?;
        }
    }

    write!(stdout, "{}{}", termion::clear::All, termion::cursor::Show)?;
    stdout.flush()?;

    Ok(())
}
<|endoftext|>


<commit_before>use chrono::DateTime;
use chrono::offset::Utc;
use diesel;
use diesel::prelude::*;
use diesel::pg::PgConnection;
use diesel::pg::upsert::*;
use futures::{Future, stream, Stream};
use futures_cpupool::CpuPool;
use hyper::{Body, Client, Uri};
use hyper::client::HttpConnector;
use hyper_tls::HttpsConnector;
use InsertError;
use kuchiki;
use kuchiki::traits::*;
use r2d2_diesel::ConnectionManager;
use r2d2::Pool;
use rusoto_core::{default_tls_client, Region};
use rusoto_credential::ProfileProvider;
use rusoto_s3::{PutObjectRequest, S3Client, S3};
use schema::ads;
use server::AdPost;

#[derive(Queryable, Debug, Clone)]
pub struct Ad {
    id: String,
    html: String,
    political: i32,
    not_political: i32,

    fuzzy_id: Option<i32>,
    title: String,
    message: String,
    thumbnail: String,

    created_at: DateTime<Utc>,
    updated_at: DateTime<Utc>,

    browser_lang: String,

    images: Vec<String>,
}

#[derive(AsChangeset)]
#[table_name = "ads"]
pub struct Images {
    thumbnail: Option<String>,
    images: Vec<String>,
}

impl Ad {
    // This will asynchronously save the images to s3 we may very well end up
    // with broken images. but I can't see any way around it right now.  Also we
    // should think about splitting this up, but I'm fine -- if a little
    // embarassed about it right now. This function swallows errors, and there's
    // a chance we'll end up with no images at the end, but I think we can
    // handle that in the extension's UI.
    pub fn grab_and_store(
        &self,
        client: Client<HttpsConnector<HttpConnector>, Body>,
        db: &Pool<ConnectionManager<PgConnection>>,
        pool: CpuPool,
    ) -> Box<Future<Item = (), Error = ()>> {
        let ad = self.clone();
        let pool_s3 = pool.clone();
        let pool_db = pool.clone();
        let db = db.clone();

        let images = [vec![self.thumbnail.clone()], self.images.clone()];
        let urls = images
            .concat()
            .iter()
            .map(|a| {
                let a: Uri = a.parse().map_err(InsertError::Uri)?;
                Ok(a)
            })
            .collect::<Vec<Result<Uri, InsertError>>>();
        let future = stream::iter(urls)
            // filter ones we already have in the db and ones we can verify as
            // coming from fb, we don't want to become a malware vector :)
            .filter(|u| match u.host() {
                Some(h) => h != "pp-facebook-ads.s3.amazonaws.com" || !h.ends_with("fbcdn.net"),
                None => false
            })
            // grab image
            .and_then(move |img| {
                info!("getting {:?}", img);
                let cloned = img.clone();
                client
                    .get(img)
                    .and_then(|res| {
                        res.body().concat2().and_then(|chunk| Ok((chunk, cloned)))
                    })
                    .map_err(InsertError::Hyper)
            })
            // upload them to s3
            .and_then(move |tuple| {
                let pool = pool_s3.clone();
                // we do this in a worker thread because rusoto isn't on
                // Hyper async yet.
                pool.spawn_fn(move || {
                    let client =
                        S3Client::new(default_tls_client().map_err(InsertError::TLS)?,
                                      ProfileProvider::new().map_err(InsertError::AWS)?,
                                      Region::UsEast1);
                    let req = PutObjectRequest {
                        bucket: "pp-facebook-ads".to_string(),
                        key: tuple.1.path().trim_left_matches("/").to_string(),
                        acl: Some("public-read".to_string()),
                        body: Some(tuple.0.to_vec()),
                        ..PutObjectRequest::default()
                    };

                    client.put_object(&req).map_err(InsertError::S3)?;
                    Ok(tuple.1)
                })
            })
            .map_err(|e| {
                warn!("{:?}", e);
                ()
            })
            .collect()
            // save the new urls to the database. the images variable will
            // include only those that we've successfully saved to s3, so we
            // have to do a funky merge here.
            .and_then(move |images| {
                let imgs = images.clone();
                pool_db.spawn_fn(move || {
                    use schema::ads::dsl::*;
                    let thumb = imgs
                        .iter()
                        .filter(|i| ad.thumbnail.contains(i.path()))
                        .map(|i| "https://pp-facebook-ads.s3.amazonaws.com/".to_string() + i.path().trim_left_matches("/"))
                        .nth(0);

                    let mut rest = imgs.clone();
                    if let Some(thumb) = thumb.clone() {
                      rest.retain(|x| !thumb.contains(x.path()))
                    };

                    let collection = rest
                        .iter()
                        .filter(|i| ad.images.iter().any(|a| a.contains(i.path())))
                        .map(|i| "https://pp-facebook-ads.s3.amazonaws.com/".to_string() + i.path().trim_left_matches("/"))
                        .collect::<Vec<String>>();

                    let update = Images {
                        thumbnail: thumb,
                        images: collection
                    };
                    // I don't understand why we're zeroing out the errors here
                    // but ok.
                    let connection = db.get().map_err(|e| {warn!("{:?}", e); ()})?;
                    diesel::update(ads.filter(id.eq(ad.id)))
                        .set(&update)
                        .execute(&*connection)
                        .map_err(|e| {warn!("{:?}", e); ()})?;
                    info!("saved {:?}", imgs);
                    Ok(())
                })
            });
        Box::new(future)
    }
}

#[derive(Insertable)]
#[table_name = "ads"]
pub struct NewAd<'a> {
    id: &'a str,
    html: &'a str,
    political: i32,
    not_political: i32,

    title: String,
    message: String,
    thumbnail: String,

    browser_lang: &'a str,
    images: Vec<String>,
}

impl<'a> NewAd<'a> {
    fn get_title(document: &kuchiki::NodeRef) -> Result<String, InsertError> {
        document
            .select("h5 a, h6 a, strong")
            .map_err(InsertError::HTML)?
            .nth(0)
            .and_then(|a| Some(a.text_contents()))
            .ok_or(InsertError::HTML(()))
    }

    fn get_image(document: &kuchiki::NodeRef) -> Result<String, InsertError> {
        document
            .select("img")
            .map_err(InsertError::HTML)?
            .nth(0)
            .and_then(|a| {
                a.attributes.borrow().get("src").and_then(
                    |src| Some(src.to_string()),
                )
            })
            .ok_or(InsertError::HTML(()))
    }
    // Video ads make this a bit messy
    fn get_message(document: &kuchiki::NodeRef) -> Result<String, InsertError> {
        let selectors = vec![".userContent p", "span"];
        let iters = selectors.iter().map(|s| document.select(s)).flat_map(|a| a);

        iters
            .map(|i| {
                i.fold(String::new(), |m, a| m + &a.as_node().to_string())
            })
            .filter(|i| i.len() > 0)
            .nth(0)
            .ok_or(InsertError::HTML(()))
    }

    fn get_images(document: &kuchiki::NodeRef) -> Result<Vec<String>, InsertError> {
        Ok(
            document
                .select("img")
                .map_err(InsertError::HTML)?
                .skip(1)
                .map(|a| {
                    a.attributes.borrow().get("src").and_then(
                        |s| if !s.contains(
                            "rsrc.php",
                        )
                        {
                            Some(s.to_string())
                        } else {
                            None
                        },
                    )
                })
                .filter(|s| s.is_some())
                .map(|s| s.unwrap())
                .collect::<Vec<String>>(),
        )
    }

    pub fn new(ad: &'a AdPost) -> Result<NewAd<'a>, InsertError> {
        let document = kuchiki::parse_html().one(ad.html.clone());

        let message = NewAd::get_message(&document)?;
        let title = NewAd::get_title(&document)?;
        let thumb = NewAd::get_image(&document)?;
        let images = NewAd::get_images(&document)?;

        Ok(NewAd {
            id: &ad.id,
            html: &ad.html,
            political: if ad.political { 1 } else { 0 },
            not_political: if !ad.political { 1 } else { 0 },
            title: title,
            message: message,
            thumbnail: thumb,
            browser_lang: &ad.browser_lang,
            images: images,
        })
    }

    pub fn save(&self, pool: &Pool<ConnectionManager<PgConnection>>) -> Result<Ad, InsertError> {
        use schema::ads;
        use schema::ads::dsl::*;
        let connection = pool.get().map_err(InsertError::Timeout)?;
        let ad: Ad = diesel::insert(&self.on_conflict(
            id,
            do_update().set((
                political.eq(political + self.political),
                not_political.eq(
                    not_political +
                        self.not_political,
                ),
                updated_at.eq(Utc::now()),
            )),
        )).into(ads::table)
            .get_result(&*connection)
            .map_err(InsertError::DataBase)?;
        Ok(ad)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ad_parsing() {
        let ad = include_str!("./html-test.txt");
        let post = AdPost {
            id: "test".to_string(),
            html: ad.to_string(),
            political: true,
            browser_lang: "en-US".to_string(),
        };
        let new_ad = NewAd::new(&post).unwrap();
        assert!(new_ad.thumbnail.len() > 0);
        assert_eq!(new_ad.images.len(), 1);
        assert_eq!(
            new_ad.message,
            "
#Valerian is “the best experience since ‘Avatar.’” See it in 3D and RealD3D theaters this Friday. Get tickets now: ValerianTickets.com
"
        );
        assert!(new_ad.title.len() > 0);
    }
}
<commit_msg>try out the ecs role<commit_after>use chrono::DateTime;
use chrono::offset::Utc;
use diesel;
use diesel::prelude::*;
use diesel::pg::PgConnection;
use diesel::pg::upsert::*;
use futures::{Future, stream, Stream};
use futures_cpupool::CpuPool;
use hyper::{Body, Client, Uri};
use hyper::client::HttpConnector;
use hyper_tls::HttpsConnector;
use InsertError;
use kuchiki;
use kuchiki::traits::*;
use r2d2_diesel::ConnectionManager;
use r2d2::Pool;
use rusoto_core::{default_tls_client, Region};
use rusoto_credential::DefaultProvider;
use rusoto_s3::{PutObjectRequest, S3Client, S3};
use schema::ads;
use server::AdPost;

#[derive(Queryable, Debug, Clone)]
pub struct Ad {
    id: String,
    html: String,
    political: i32,
    not_political: i32,

    fuzzy_id: Option<i32>,
    title: String,
    message: String,
    thumbnail: String,

    created_at: DateTime<Utc>,
    updated_at: DateTime<Utc>,

    browser_lang: String,

    images: Vec<String>,
}

#[derive(AsChangeset)]
#[table_name = "ads"]
pub struct Images {
    thumbnail: Option<String>,
    images: Vec<String>,
}

impl Ad {
    // This will asynchronously save the images to s3 we may very well end up
    // with broken images. but I can't see any way around it right now.  Also we
    // should think about splitting this up, but I'm fine -- if a little
    // embarassed about it right now. This function swallows errors, and there's
    // a chance we'll end up with no images at the end, but I think we can
    // handle that in the extension's UI.
    pub fn grab_and_store(
        &self,
        client: Client<HttpsConnector<HttpConnector>, Body>,
        db: &Pool<ConnectionManager<PgConnection>>,
        pool: CpuPool,
    ) -> Box<Future<Item = (), Error = ()>> {
        let ad = self.clone();
        let pool_s3 = pool.clone();
        let pool_db = pool.clone();
        let db = db.clone();

        let images = [vec![self.thumbnail.clone()], self.images.clone()];
        let urls = images
            .concat()
            .iter()
            .map(|a| {
                let a: Uri = a.parse().map_err(InsertError::Uri)?;
                Ok(a)
            })
            .collect::<Vec<Result<Uri, InsertError>>>();
        let future = stream::iter(urls)
            // filter ones we already have in the db and ones we can verify as
            // coming from fb, we don't want to become a malware vector :)
            .filter(|u| match u.host() {
                Some(h) => h != "pp-facebook-ads.s3.amazonaws.com" || !h.ends_with("fbcdn.net"),
                None => false
            })
            // grab image
            .and_then(move |img| {
                info!("getting {:?}", img);
                let cloned = img.clone();
                client
                    .get(img)
                    .and_then(|res| {
                        res.body().concat2().and_then(|chunk| Ok((chunk, cloned)))
                    })
                    .map_err(InsertError::Hyper)
            })
            // upload them to s3
            .and_then(move |tuple| {
                let pool = pool_s3.clone();
                // we do this in a worker thread because rusoto isn't on
                // Hyper async yet.
                pool.spawn_fn(move || {
                    let client =
                        S3Client::new(default_tls_client().map_err(InsertError::TLS)?,
                                      DefaultProvider::new().map_err(InsertError::AWS)?,
                                      Region::UsEast1);
                    let req = PutObjectRequest {
                        bucket: "pp-facebook-ads".to_string(),
                        key: tuple.1.path().trim_left_matches("/").to_string(),
                        acl: Some("public-read".to_string()),
                        body: Some(tuple.0.to_vec()),
                        ..PutObjectRequest::default()
                    };

                    client.put_object(&req).map_err(InsertError::S3)?;
                    Ok(tuple.1)
                })
            })
            .map_err(|e| {
                warn!("{:?}", e);
                ()
            })
            .collect()
            // save the new urls to the database. the images variable will
            // include only those that we've successfully saved to s3, so we
            // have to do a funky merge here.
            .and_then(move |images| {
                let imgs = images.clone();
                pool_db.spawn_fn(move || {
                    use schema::ads::dsl::*;
                    let thumb = imgs
                        .iter()
                        .filter(|i| ad.thumbnail.contains(i.path()))
                        .map(|i| "https://pp-facebook-ads.s3.amazonaws.com/".to_string() + i.path().trim_left_matches("/"))
                        .nth(0);

                    let mut rest = imgs.clone();
                    if let Some(thumb) = thumb.clone() {
                      rest.retain(|x| !thumb.contains(x.path()))
                    };

                    let collection = rest
                        .iter()
                        .filter(|i| ad.images.iter().any(|a| a.contains(i.path())))
                        .map(|i| "https://pp-facebook-ads.s3.amazonaws.com/".to_string() + i.path().trim_left_matches("/"))
                        .collect::<Vec<String>>();

                    let update = Images {
                        thumbnail: thumb,
                        images: collection
                    };
                    // I don't understand why we're zeroing out the errors here
                    // but ok.
                    let connection = db.get().map_err(|e| {warn!("{:?}", e); ()})?;
                    diesel::update(ads.filter(id.eq(ad.id)))
                        .set(&update)
                        .execute(&*connection)
                        .map_err(|e| {warn!("{:?}", e); ()})?;
                    info!("saved {:?}", imgs);
                    Ok(())
                })
            });
        Box::new(future)
    }
}

#[derive(Insertable)]
#[table_name = "ads"]
pub struct NewAd<'a> {
    id: &'a str,
    html: &'a str,
    political: i32,
    not_political: i32,

    title: String,
    message: String,
    thumbnail: String,

    browser_lang: &'a str,
    images: Vec<String>,
}

impl<'a> NewAd<'a> {
    fn get_title(document: &kuchiki::NodeRef) -> Result<String, InsertError> {
        document
            .select("h5 a, h6 a, strong")
            .map_err(InsertError::HTML)?
            .nth(0)
            .and_then(|a| Some(a.text_contents()))
            .ok_or(InsertError::HTML(()))
    }

    fn get_image(document: &kuchiki::NodeRef) -> Result<String, InsertError> {
        document
            .select("img")
            .map_err(InsertError::HTML)?
            .nth(0)
            .and_then(|a| {
                a.attributes.borrow().get("src").and_then(
                    |src| Some(src.to_string()),
                )
            })
            .ok_or(InsertError::HTML(()))
    }
    // Video ads make this a bit messy
    fn get_message(document: &kuchiki::NodeRef) -> Result<String, InsertError> {
        let selectors = vec![".userContent p", "span"];
        let iters = selectors.iter().map(|s| document.select(s)).flat_map(|a| a);

        iters
            .map(|i| {
                i.fold(String::new(), |m, a| m + &a.as_node().to_string())
            })
            .filter(|i| i.len() > 0)
            .nth(0)
            .ok_or(InsertError::HTML(()))
    }

    fn get_images(document: &kuchiki::NodeRef) -> Result<Vec<String>, InsertError> {
        Ok(
            document
                .select("img")
                .map_err(InsertError::HTML)?
                .skip(1)
                .map(|a| {
                    a.attributes.borrow().get("src").and_then(
                        |s| if !s.contains(
                            "rsrc.php",
                        )
                        {
                            Some(s.to_string())
                        } else {
                            None
                        },
                    )
                })
                .filter(|s| s.is_some())
                .map(|s| s.unwrap())
                .collect::<Vec<String>>(),
        )
    }

    pub fn new(ad: &'a AdPost) -> Result<NewAd<'a>, InsertError> {
        let document = kuchiki::parse_html().one(ad.html.clone());

        let message = NewAd::get_message(&document)?;
        let title = NewAd::get_title(&document)?;
        let thumb = NewAd::get_image(&document)?;
        let images = NewAd::get_images(&document)?;

        Ok(NewAd {
            id: &ad.id,
            html: &ad.html,
            political: if ad.political { 1 } else { 0 },
            not_political: if !ad.political { 1 } else { 0 },
            title: title,
            message: message,
            thumbnail: thumb,
            browser_lang: &ad.browser_lang,
            images: images,
        })
    }

    pub fn save(&self, pool: &Pool<ConnectionManager<PgConnection>>) -> Result<Ad, InsertError> {
        use schema::ads;
        use schema::ads::dsl::*;
        let connection = pool.get().map_err(InsertError::Timeout)?;
        let ad: Ad = diesel::insert(&self.on_conflict(
            id,
            do_update().set((
                political.eq(political + self.political),
                not_political.eq(
                    not_political +
                        self.not_political,
                ),
                updated_at.eq(Utc::now()),
            )),
        )).into(ads::table)
            .get_result(&*connection)
            .map_err(InsertError::DataBase)?;
        Ok(ad)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ad_parsing() {
        let ad = include_str!("./html-test.txt");
        let post = AdPost {
            id: "test".to_string(),
            html: ad.to_string(),
            political: true,
            browser_lang: "en-US".to_string(),
        };
        let new_ad = NewAd::new(&post).unwrap();
        assert!(new_ad.thumbnail.len() > 0);
        assert_eq!(new_ad.images.len(), 1);
        assert_eq!(
            new_ad.message,
            "
#Valerian is “the best experience since ‘Avatar.’” See it in 3D and RealD3D theaters this Friday. Get tickets now: ValerianTickets.com
"
        );
        assert!(new_ad.title.len() > 0);
    }
}
<|endoftext|>


<commit_before>use prodbg_api::view::CViewCallbacks;
use std::rc::Rc;
use plugin::Plugin;
use plugins::PluginHandler;
use dynamic_reload::Lib;
use session::SessionHandle;
use std::ptr;
use std::os::raw::{c_void, c_uchar};
use prodbg_api::ui::Ui;
use plugin_io;

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct ViewHandle(pub u64);

pub struct ViewInstance {
    pub plugin_data: *mut c_void,
    pub ui: Ui,
    pub name: String,
    pub handle: ViewHandle,
    pub session_handle: SessionHandle,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    pub plugin_type: Rc<Plugin>,
}

#[derive(Clone)]
struct ReloadState {
    name: String,
    ui: Ui,
    handle: ViewHandle,
    session_handle: SessionHandle,
}

pub struct ViewPlugins {
    pub instances: Vec<ViewInstance>,
    plugin_types: Vec<Rc<Plugin>>,
    reload_state: Vec<ReloadState>,
    handle_counter: ViewHandle,
}

impl PluginHandler for ViewPlugins {
    fn is_correct_plugin_type(&self, plugin: &Plugin) -> bool {
        plugin.type_name.contains("View")
    }

    fn add_plugin(&mut self, plugin: &Rc<Plugin>) {
        println!("added plugin type {}", plugin.type_name);
        self.plugin_types.push(plugin.clone())
    }

    fn unload_plugin(&mut self, lib: &Rc<Lib>) {
        self.reload_state.clear();
        for i in (0..self.instances.len()).rev() {
            if &self.instances[i].plugin_type.lib == lib {
                let state = ReloadState {
                    ui: self.instances[i].ui.clone(),
                    name: self.instances[i].plugin_type.name.clone(),
                    handle: self.instances[i].handle,
                    session_handle: self.instances[i].session_handle,
                };

                self.reload_state.push(state);
                self.instances.swap_remove(i);
            }
        }

        for i in (0..self.plugin_types.len()).rev() {
            if &self.plugin_types[i].lib == lib {
                self.plugin_types.swap_remove(i);
            }
        }
    }

    fn reload_plugin(&mut self) {
        let t = self.reload_state.clone();
        for reload_plugin in &t {
            self.create_instance_with_handle(
                                  reload_plugin.ui.clone(),
                                  &reload_plugin.name,
                                  &None, // TODO: Include saved data here
                                  reload_plugin.session_handle,
                                  reload_plugin.handle);
        }
    }

    fn reload_failed(&mut self) {}
}

impl ViewPlugins {
    pub fn new() -> ViewPlugins {
        ViewPlugins {
            instances: Vec::new(),
            plugin_types: Vec::new(),
            reload_state: Vec::new(),
            handle_counter: ViewHandle(0),
        }
    }

    pub extern "C" fn service_fun(_name: *const c_uchar) -> *mut c_void {
        ptr::null_mut()
    }

    pub fn get_view(&mut self, view_handle: ViewHandle) -> Option<&mut ViewInstance> {
        for i in 0..self.instances.len() {
            if self.instances[i].handle == view_handle {
                return Some(&mut self.instances[i]);
            }
        }

        None
    }

    pub fn create_instance_from_index(&mut self,
                                      ui: Ui,
                                      index: usize,
                                      session_handle: SessionHandle,
                                      view_handle: Option<ViewHandle>)
                                      -> Option<ViewHandle> {
        let plugin_data = unsafe {
            let callbacks = self.plugin_types[index].plugin_funcs as *mut CViewCallbacks;
            (*callbacks).create_instance.unwrap()(ui.api as *mut c_void, Self::service_fun)
        };

        let handle = match view_handle {
            Some(h) => {
                if h.0 >= self.handle_counter.0 {
                    self.handle_counter.0 = h.0 + 1
                }
                h
            },
            _ => {
                let counter = self.handle_counter;
                self.handle_counter.0 += 1;
                counter
            }
        };

        let instance = ViewInstance {
            plugin_data: plugin_data,
            name: format!("Plugin {}", handle.0),
            ui: ui,
            handle: handle,
            session_handle: session_handle,
            x: 0.0,
            y: 0.0,
            width: 0.0,
            height: 0.0,
            plugin_type: self.plugin_types[index].clone(),
        };

        self.instances.push(instance);

        Some(handle)
    }

    pub fn create_instance(&mut self,
                           ui: Ui,
                           plugin_type: &String,
                           session_handle: SessionHandle)
                           -> Option<ViewHandle> {
        for i in 0..self.plugin_types.len() {
            if self.plugin_types[i].name != *plugin_type {
                continue;
            }

            return Self::create_instance_from_index(self, ui, i, session_handle, None);
        }

        None
    }

    pub fn create_instance_with_handle(&mut self,
                                       ui: Ui,
                                       plugin_type: &String,
                                       plugin_data: &Option<Vec<String>>,
                                       session_handle: SessionHandle,
                                       view_handle: ViewHandle) -> Option<ViewHandle> {

        for i in 0..self.plugin_types.len() {
            if self.plugin_types[i].name != *plugin_type {
                continue;
            }

            let handle = Self::create_instance_from_index(self, ui, i, session_handle, Some(view_handle));

            if let Some(ref data) = *plugin_data {
                self.get_view(handle.unwrap()).map(|instance| {
                    instance.load_plugin_data(&data);
                });
            }

            return handle;
        }

        None
    }

    pub fn destroy_instance(&mut self, handle: ViewHandle) {
        for i in (0..self.instances.len()).rev() {
            if self.instances[i].handle.0 == handle.0 {
                self.instances.swap_remove(i);
                return;
            }
        }
    }

    // TODO: Would be nice to use something stack-base instead or return an iterator to interate
    // over the data instead
    pub fn get_plugin_names(&self) -> Vec<String> {
        let mut names = Vec::new();

        for i in &self.plugin_types {
            names.push(i.name.clone());
        }

        names
    }
}

impl ViewInstance {
    pub fn get_plugin_data(&self) -> (String, Option<Vec<String>>) {
        let mut plugin_data = None;
        unsafe {
            let callbacks = self.plugin_type.plugin_funcs as *mut CViewCallbacks;
            if let Some(save_state) = (*callbacks).save_state {
                let mut writer_funcs = plugin_io::get_writer_funcs();
                save_state(self.plugin_data, &mut writer_funcs);
                plugin_data = Some(plugin_io::get_data(&mut writer_funcs));
            }
        };
        (self.plugin_type.name.clone(), plugin_data)
    }

    pub fn load_plugin_data(&mut self, data: &Vec<String>) {
        unsafe {
            let callbacks = self.plugin_type.plugin_funcs as *mut CViewCallbacks;
            if let Some(load_state) = (*callbacks).load_state {
                let mut loader_funcs = plugin_io::get_loader_funcs(data);
                load_state(self.plugin_data, &mut loader_funcs);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_search_paths_none() {}
}
<commit_msg>Use the correct get_service function<commit_after>use prodbg_api::view::CViewCallbacks;
use std::rc::Rc;
use plugin::Plugin;
use plugins::PluginHandler;
use dynamic_reload::Lib;
use session::SessionHandle;
use std::os::raw::{c_void};
use prodbg_api::ui::Ui;
use services;
use plugin_io;

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct ViewHandle(pub u64);

pub struct ViewInstance {
    pub plugin_data: *mut c_void,
    pub ui: Ui,
    pub name: String,
    pub handle: ViewHandle,
    pub session_handle: SessionHandle,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    pub plugin_type: Rc<Plugin>,
}

#[derive(Clone)]
struct ReloadState {
    name: String,
    ui: Ui,
    handle: ViewHandle,
    session_handle: SessionHandle,
}

pub struct ViewPlugins {
    pub instances: Vec<ViewInstance>,
    plugin_types: Vec<Rc<Plugin>>,
    reload_state: Vec<ReloadState>,
    handle_counter: ViewHandle,
}

impl PluginHandler for ViewPlugins {
    fn is_correct_plugin_type(&self, plugin: &Plugin) -> bool {
        plugin.type_name.contains("View")
    }

    fn add_plugin(&mut self, plugin: &Rc<Plugin>) {
        println!("added plugin type {}", plugin.type_name);
        self.plugin_types.push(plugin.clone())
    }

    fn unload_plugin(&mut self, lib: &Rc<Lib>) {
        self.reload_state.clear();
        for i in (0..self.instances.len()).rev() {
            if &self.instances[i].plugin_type.lib == lib {
                let state = ReloadState {
                    ui: self.instances[i].ui.clone(),
                    name: self.instances[i].plugin_type.name.clone(),
                    handle: self.instances[i].handle,
                    session_handle: self.instances[i].session_handle,
                };

                self.reload_state.push(state);
                self.instances.swap_remove(i);
            }
        }

        for i in (0..self.plugin_types.len()).rev() {
            if &self.plugin_types[i].lib == lib {
                self.plugin_types.swap_remove(i);
            }
        }
    }

    fn reload_plugin(&mut self) {
        let t = self.reload_state.clone();
        for reload_plugin in &t {
            self.create_instance_with_handle(
                                  reload_plugin.ui.clone(),
                                  &reload_plugin.name,
                                  &None, // TODO: Include saved data here
                                  reload_plugin.session_handle,
                                  reload_plugin.handle);
        }
    }

    fn reload_failed(&mut self) {}
}

impl ViewPlugins {
    pub fn new() -> ViewPlugins {
        ViewPlugins {
            instances: Vec::new(),
            plugin_types: Vec::new(),
            reload_state: Vec::new(),
            handle_counter: ViewHandle(0),
        }
    }

    pub fn get_view(&mut self, view_handle: ViewHandle) -> Option<&mut ViewInstance> {
        for i in 0..self.instances.len() {
            if self.instances[i].handle == view_handle {
                return Some(&mut self.instances[i]);
            }
        }

        None
    }

    pub fn create_instance_from_index(&mut self,
                                      ui: Ui,
                                      index: usize,
                                      session_handle: SessionHandle,
                                      view_handle: Option<ViewHandle>)
                                      -> Option<ViewHandle> {
        let plugin_data = unsafe {
            let callbacks = self.plugin_types[index].plugin_funcs as *mut CViewCallbacks;
            (*callbacks).create_instance.unwrap()(ui.api as *mut c_void, services::get_services)
        };

        let handle = match view_handle {
            Some(h) => {
                if h.0 >= self.handle_counter.0 {
                    self.handle_counter.0 = h.0 + 1
                }
                h
            },
            _ => {
                let counter = self.handle_counter;
                self.handle_counter.0 += 1;
                counter
            }
        };

        let instance = ViewInstance {
            plugin_data: plugin_data,
            name: format!("Plugin {}", handle.0),
            ui: ui,
            handle: handle,
            session_handle: session_handle,
            x: 0.0,
            y: 0.0,
            width: 0.0,
            height: 0.0,
            plugin_type: self.plugin_types[index].clone(),
        };

        self.instances.push(instance);

        Some(handle)
    }

    pub fn create_instance(&mut self,
                           ui: Ui,
                           plugin_type: &String,
                           session_handle: SessionHandle)
                           -> Option<ViewHandle> {
        for i in 0..self.plugin_types.len() {
            if self.plugin_types[i].name != *plugin_type {
                continue;
            }

            return Self::create_instance_from_index(self, ui, i, session_handle, None);
        }

        None
    }

    pub fn create_instance_with_handle(&mut self,
                                       ui: Ui,
                                       plugin_type: &String,
                                       plugin_data: &Option<Vec<String>>,
                                       session_handle: SessionHandle,
                                       view_handle: ViewHandle) -> Option<ViewHandle> {

        for i in 0..self.plugin_types.len() {
            if self.plugin_types[i].name != *plugin_type {
                continue;
            }

            let handle = Self::create_instance_from_index(self, ui, i, session_handle, Some(view_handle));

            if let Some(ref data) = *plugin_data {
                self.get_view(handle.unwrap()).map(|instance| {
                    instance.load_plugin_data(&data);
                });
            }

            return handle;
        }

        None
    }

    pub fn destroy_instance(&mut self, handle: ViewHandle) {
        for i in (0..self.instances.len()).rev() {
            if self.instances[i].handle.0 == handle.0 {
                self.instances.swap_remove(i);
                return;
            }
        }
    }

    // TODO: Would be nice to use something stack-base instead or return an iterator to interate
    // over the data instead
    pub fn get_plugin_names(&self) -> Vec<String> {
        let mut names = Vec::new();

        for i in &self.plugin_types {
            names.push(i.name.clone());
        }

        names
    }
}

impl ViewInstance {
    pub fn get_plugin_data(&self) -> (String, Option<Vec<String>>) {
        let mut plugin_data = None;
        unsafe {
            let callbacks = self.plugin_type.plugin_funcs as *mut CViewCallbacks;
            if let Some(save_state) = (*callbacks).save_state {
                let mut writer_funcs = plugin_io::get_writer_funcs();
                save_state(self.plugin_data, &mut writer_funcs);
                plugin_data = Some(plugin_io::get_data(&mut writer_funcs));
            }
        };
        (self.plugin_type.name.clone(), plugin_data)
    }

    pub fn load_plugin_data(&mut self, data: &Vec<String>) {
        unsafe {
            let callbacks = self.plugin_type.plugin_funcs as *mut CViewCallbacks;
            if let Some(load_state) = (*callbacks).load_state {
                let mut loader_funcs = plugin_io::get_loader_funcs(data);
                load_state(self.plugin_data, &mut loader_funcs);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_search_paths_none() {}
}
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2017 Boucher, Antoni <bouanto@zoho.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#![feature(proc_macro)]

extern crate gtk;
#[macro_use]
extern crate relm;
extern crate relm_attributes;
#[macro_use]
extern crate relm_derive;
#[macro_use]
extern crate relm_test;

use gtk::{
    ButtonExt,
    Inhibit,
    LabelExt,
    OrientableExt,
    WidgetExt,
};
use gtk::Orientation::Vertical;
use relm::Widget;
use relm_attributes::widget;

use self::Msg::*;

pub struct Model {
    counter: i32,
}

#[derive(Msg)]
pub enum Msg {
    Decrement,
    Increment,
    Quit,
}

#[widget]
impl Widget for Win {
    fn model() -> Model {
        Model {
            counter: 0,
        }
    }

    fn update(&mut self, event: Msg) {
        match event {
            Decrement => self.model.counter -= 1,
            Increment => self.model.counter += 1,
            Quit => gtk::main_quit(),
        }
    }

    view! {
        gtk::Window {
            gtk::Box {
                orientation: Vertical,
                #[name="inc_button"]
                gtk::Button {
                    clicked => Increment,
                    label: "+",
                },
                #[name="label"]
                gtk::Label {
                    text: &self.model.counter.to_string(),
                },
                #[name="dec_button"]
                gtk::Button {
                    clicked => Decrement,
                    label: "-",
                },
            },
            delete_event(_, _) => (Quit, Inhibit(false)),
        }
    }
}

#[cfg(test)]
mod tests {
    use relm;
    use relm_test::click;

    use super::Win;

    #[test]
    fn label_change() {
        // TODO: find a way to make it work: get the real component?
        /*let component = relm::init_test::<Win>(()).unwrap();
        let inc_button = component.widget().inc_button.clone();
        let dec_button = component.widget().dec_button.clone();

        assert_text!(component.widget().label, 0);
        click(&inc_button);
        assert_text!(component.widget().label, 1);
        click(&inc_button);
        assert_text!(component.widget().label, 2);
        click(&dec_button);
        assert_text!(component.widget().label, 1);
        click(&dec_button);
        assert_text!(component.widget().label, 0);
        click(&dec_button);
        assert_text!(component.widget().label, -1);*/
    }
}
<commit_msg>Comment unused test<commit_after>/*
 * Copyright (c) 2017 Boucher, Antoni <bouanto@zoho.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#![feature(proc_macro)]

extern crate gtk;
#[macro_use]
extern crate relm;
extern crate relm_attributes;
#[macro_use]
extern crate relm_derive;
extern crate relm_test;

use gtk::{
    ButtonExt,
    Inhibit,
    LabelExt,
    OrientableExt,
    WidgetExt,
};
use gtk::Orientation::Vertical;
use relm::Widget;
use relm_attributes::widget;

use self::Msg::*;

pub struct Model {
    counter: i32,
}

#[derive(Msg)]
pub enum Msg {
    Decrement,
    Increment,
    Quit,
}

#[widget]
impl Widget for Win {
    fn model() -> Model {
        Model {
            counter: 0,
        }
    }

    fn update(&mut self, event: Msg) {
        match event {
            Decrement => self.model.counter -= 1,
            Increment => self.model.counter += 1,
            Quit => gtk::main_quit(),
        }
    }

    view! {
        gtk::Window {
            gtk::Box {
                orientation: Vertical,
                #[name="inc_button"]
                gtk::Button {
                    clicked => Increment,
                    label: "+",
                },
                #[name="label"]
                gtk::Label {
                    text: &self.model.counter.to_string(),
                },
                #[name="dec_button"]
                gtk::Button {
                    clicked => Decrement,
                    label: "-",
                },
            },
            delete_event(_, _) => (Quit, Inhibit(false)),
        }
    }
}

// #[cfg(test)]
// mod tests {
//     use relm;
//     use relm_test::click;

//     use super::Win;

//     #[test]
//     fn label_change() {
//         // TODO: find a way to make it work: get the real component?
//         let component = relm::init_test::<Win>(()).unwrap();
//         let inc_button = component.widget().inc_button.clone();
//         let dec_button = component.widget().dec_button.clone();

//         assert_text!(component.widget().label, 0);
//         click(&inc_button);
//         assert_text!(component.widget().label, 1);
//         click(&inc_button);
//         assert_text!(component.widget().label, 2);
//         click(&dec_button);
//         assert_text!(component.widget().label, 1);
//         click(&dec_button);
//         assert_text!(component.widget().label, 0);
//         click(&dec_button);
//         assert_text!(component.widget().label, -1);
//     }
// }
<|endoftext|>


<commit_before>extern crate rosc;

#[cfg(feature = "std")]
use rosc::address::Matcher;

#[cfg(feature = "std")]
#[test]
fn test_matcher() {
    let matcher = Matcher::new("/oscillator/[0-9]/*/pre[!1234?*]post/{frequency,phase}/x?")
        .expect("Matcher::new");
    assert_eq!(
        matcher
            .match_address("/oscillator/1/something/preXpost/phase/xy")
            .expect("should match"),
        true
    );
    assert_eq!(
        matcher
            .match_address("/oscillator/1/something/pre1post/phase/xy")
            .expect("should not match"),
        false
    );
    matcher.match_address("invalid_address").expect_err("should fail because address does not start with a slash");
}

#[cfg(feature = "std")]
#[test]
fn test_bad_address_pattern() {
    let expected_err = "bad OSC address pattern: bad address pattern";
    assert_eq!(Matcher::new("").unwrap_err().to_string(), expected_err);
    assert_eq!(Matcher::new("/").unwrap_err().to_string(), expected_err);
    assert_eq!(Matcher::new("//empty/parts/").unwrap_err().to_string(), expected_err);
    assert_eq!(Matcher::new("////").unwrap_err().to_string(), expected_err);
    assert_eq!(Matcher::new("/{unclosed,alternative").unwrap_err().to_string(), expected_err);
    assert_eq!(Matcher::new("/unclosed/[range-").unwrap_err().to_string(), expected_err);
}

#[cfg(feature = "std")]
#[test]
fn test_bad_address() {
    let matcher = Matcher::new("/does-not-matter").expect("Matcher::new");
    let expected_err = "bad OSC address: bad address";
    assert_eq!(matcher.match_address("").unwrap_err().to_string(), expected_err);
    assert_eq!(matcher.match_address("/").unwrap_err().to_string(), expected_err);
    assert_eq!(matcher.match_address("/contains/wildcards?").unwrap_err().to_string(), expected_err);
    assert_eq!(matcher.match_address("/contains/wildcards*").unwrap_err().to_string(), expected_err);
    assert_eq!(matcher.match_address("/contains/ranges[a-z]").unwrap_err().to_string(), expected_err);
    assert_eq!(matcher.match_address("/contains/ranges[!a-z]").unwrap_err().to_string(), expected_err);
    assert_eq!(matcher.match_address("/{contains,alternative}").unwrap_err().to_string(), expected_err);
}
<commit_msg>Add tests<commit_after>extern crate rosc;

#[cfg(feature = "std")]
use rosc::address::Matcher;

#[cfg(feature = "std")]
#[test]
fn test_matcher_new() {
    let mut matcher;

    // Regular address using only alphanumeric parts
    matcher = Matcher::new("/oscillator/1/frequency").expect("Should be valid");
    matcher.match_address("/oscillator/1/frequency").expect("Should match");
    matcher.match_address("/oscillator/1/phase").expect_err("Should not match");
    matcher.match_address("/oscillator/1/frequencyfoo").expect_err("Should not match");
    matcher.match_address("/prefix/oscillator/1/frequency").expect_err("Should not match");

    // Choice
    matcher = Matcher::new("/foo{bar,baz}").expect("Should be valid");
    matcher.match_address("/foobar").expect("Should match");
    matcher.match_address("/foobaz").expect("Should match");

    matcher = Matcher::new("/foo{bar,baz,tron}").expect("Should be valid");
    matcher.match_address("/footron").expect("Should match");

    // Character class
    // Character classes are sets or ranges of characters to match.
    // e.g. [a-z] will match any lower case alphabetic character. [abcd] will match the characters abcd.
    // They can be negated with '!', e.g. [!0-9] will match all characters except 0-9
    // Basic example
    matcher = Matcher::new("/oscillator/[0-9]").expect("Should be valid");
    matcher.match_address("/oscillator/0").expect("Should match");  // Beginning of range included
    matcher.match_address("/oscillator/6").expect("Should match");  // Middle of range
    matcher.match_address("/oscillator/9").expect("Should match");  // Last member of range included

    // Inverted order should work too
    matcher = Matcher::new("/oscillator/[9-0]").expect("Should be valid");
    matcher.match_address("/oscillator/0").expect("Should match");
    matcher.match_address("/oscillator/6").expect("Should match");
    matcher.match_address("/oscillator/9").expect("Should match");

    // Multiple ranges
    matcher = Matcher::new("/oscillator/[a-zA-Z0-9]").expect("Should be valid");
    matcher.match_address("/oscillator/0").expect("Should match");
    matcher.match_address("/oscillator/a").expect("Should match");
    matcher.match_address("/oscillator/A").expect("Should match");

    // Negated range
    matcher = Matcher::new("/oscillator/[!0-9]").expect("Should be valid");
    matcher.match_address("/oscillator/1").expect_err("Should not match");
    matcher.match_address("/oscillator/a").expect("Should match");

    // Extra exclamation points must be entirely ignored
    matcher = Matcher::new("/oscillator/[!0-9!a-z!]").expect("Should be valid");
    matcher.match_address("/oscillator/A").expect("Should match");

    // Trailing dash has no special meaning
    matcher = Matcher::new("/oscillator/[abcd-]").expect("Should be valid");
    matcher.match_address("/oscillator/a").expect("Should match");
    matcher.match_address("/oscillator/-").expect("Should match");

    // Single wildcard
    // A single wildcard '?' matches excatly one alphanumeric character
    matcher = Matcher::new("/oscillator/?/frequency").expect("Should be valid");
    matcher.match_address("/oscillator/1/frequency").expect("Should match");
    matcher.match_address("/oscillator/F/frequency").expect("Should match");
    matcher.match_address("/oscillator//frequency").expect_err("Should not match");
    matcher.match_address("/oscillator/10/frequency").expect_err("Should not match");

    // Test if two consecutive wildcards match
    matcher = Matcher::new("/oscillator/??/frequency").expect("Should be valid");
    matcher.match_address("/oscillator/10/frequency").expect("Should match");
    matcher.match_address("/oscillator/1/frequency").expect_err("Should not match");

    // Test if it works if it is surrounded by non-wildcards
    matcher = Matcher::new("/oscillator/prefixed?postfixed/frequency").expect("Should be valid");
    matcher.match_address("/oscillator/prefixed1postfixed/frequency").expect("Should match");
    matcher.match_address("/oscillator/prefixedpostfixed/frequency").expect_err("Should not match");

    // Wildcard
    // Wildcards '*' match zero or more alphanumeric characters. The implementation is greedy,
    // meaning it will match the longest possible sequence
    matcher = Matcher::new("/oscillator/*/frequency").expect("Should be valid");
    matcher.match_address("/oscillator/anything123/frequency").expect("Should match");
    // Test that wildcard doesn't cross part boundary
    matcher.match_address("/oscillator/extra/part/frequency").expect_err("Should not match");
    matcher.match_address("/oscillator//frequency").expect_err("Should not match");

    // Test greediness
    matcher = Matcher::new("/oscillator/*bar/frequency").expect("Should be valid");
    matcher.match_address("/oscillator/foobar/frequency").expect("Should match");
    matcher.match_address("/oscillator/foobarbar/frequency").expect("Should match");

    // Minimum length of 2
    matcher = Matcher::new("/oscillator/*??/frequency").expect("Should be valid");
    matcher.match_address("/oscillator/foobar/frequency").expect("Should match");
    matcher.match_address("/oscillator/f/frequency").expect_err("Should not match");

    // Minimum length of 2 and another component follows
    matcher = Matcher::new("/oscillator/*??baz/frequency").expect("Should be valid");
    matcher.match_address("/oscillator/foobarbaz/frequency").expect("Should match");
    matcher.match_address("/oscillator/fbaz/frequency").expect_err("Should not match");

    // Mix with character class
    matcher = Matcher::new("/oscillator/*[a-d]/frequency").expect("Should be valid");
    matcher.match_address("/oscillator/a/frequency").expect("Should match");
    matcher.match_address("/oscillator/fooa/frequency").expect("Should match");
    matcher.match_address("/oscillator/foox/frequency").expect_err("Should not match");

    // Mix with choice
    matcher = Matcher::new("/oscillator/*{bar,baz}/frequency").expect("Should be valid");
    matcher.match_address("/oscillator/foobar/frequency").expect("Should match");
    matcher.match_address("/oscillator/baz/frequency").expect("Should match");
    matcher.match_address("/oscillator/something/frequency").expect_err("Should not match");
}

#[cfg(feature = "std")]
#[test]
fn test_bad_address_pattern() {
    let expected_err = "bad OSC address pattern: bad address pattern";
    assert_eq!(Matcher::new("").unwrap_err().to_string(), expected_err);
    assert_eq!(Matcher::new("/").unwrap_err().to_string(), expected_err);
    assert_eq!(Matcher::new("//empty/parts/").unwrap_err().to_string(), expected_err);
    assert_eq!(Matcher::new("////").unwrap_err().to_string(), expected_err);
    assert_eq!(Matcher::new("/{unclosed,alternative").unwrap_err().to_string(), expected_err);
    assert_eq!(Matcher::new("/unclosed/[range-").unwrap_err().to_string(), expected_err);
}

#[cfg(feature = "std")]
#[test]
fn test_bad_address() {
    let matcher = Matcher::new("/does-not-matter").expect("Matcher::new");
    let expected_err = "bad OSC address: bad address";
    assert_eq!(matcher.match_address("").unwrap_err().to_string(), expected_err);
    assert_eq!(matcher.match_address("/").unwrap_err().to_string(), expected_err);
    assert_eq!(matcher.match_address("/contains/wildcards?").unwrap_err().to_string(), expected_err);
    assert_eq!(matcher.match_address("/contains/wildcards*").unwrap_err().to_string(), expected_err);
    assert_eq!(matcher.match_address("/contains/ranges[a-z]").unwrap_err().to_string(), expected_err);
    assert_eq!(matcher.match_address("/contains/ranges[!a-z]").unwrap_err().to_string(), expected_err);
    assert_eq!(matcher.match_address("/{contains,alternative}").unwrap_err().to_string(), expected_err);
}
<|endoftext|>


<commit_before>#![feature(test)]

use compiletest_rs as compiletest;
extern crate test;

use std::env::{set_var, var};
use std::ffi::OsStr;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

fn clippy_driver_path() -> PathBuf {
    if let Some(path) = option_env!("CLIPPY_DRIVER_PATH") {
        PathBuf::from(path)
    } else {
        PathBuf::from(concat!("target/", env!("PROFILE"), "/clippy-driver"))
    }
}

fn host_libs() -> PathBuf {
    if let Some(path) = option_env!("HOST_LIBS") {
        PathBuf::from(path)
    } else {
        Path::new("target").join(env!("PROFILE"))
    }
}

fn rustc_test_suite() -> Option<PathBuf> {
    option_env!("RUSTC_TEST_SUITE").map(PathBuf::from)
}

fn rustc_lib_path() -> PathBuf {
    option_env!("RUSTC_LIB_PATH").unwrap().into()
}

fn config(mode: &str, dir: PathBuf) -> compiletest::Config {
    let mut config = compiletest::Config::default();

    let cfg_mode = mode.parse().expect("Invalid mode");
    if let Ok(name) = var::<&str>("TESTNAME") {
        let s: String = name.to_owned();
        config.filter = Some(s)
    }

    if rustc_test_suite().is_some() {
        config.run_lib_path = rustc_lib_path();
        config.compile_lib_path = rustc_lib_path();
    }
    config.target_rustcflags = Some(format!(
        "-L {0} -L {0}/deps -Dwarnings -Zui-testing",
        host_libs().display()
    ));

    config.mode = cfg_mode;
    config.build_base = if rustc_test_suite().is_some() {
        // we don't need access to the stderr files on travis
        let mut path = PathBuf::from(env!("OUT_DIR"));
        path.push("test_build_base");
        path
    } else {
        let mut path = std::env::current_dir().unwrap();
        path.push("target/debug/test_build_base");
        path
    };
    config.src_base = dir;
    config.rustc_path = clippy_driver_path();
    config
}

fn run_mode(mode: &str, dir: PathBuf) {
    let cfg = config(mode, dir);
    // clean rmeta data, otherwise "cargo check; cargo test" fails (#2896)
    cfg.clean_rmeta();
    compiletest::run_tests(&cfg);
}

fn run_ui_toml_tests(config: &compiletest::Config, mut tests: Vec<test::TestDescAndFn>) -> Result<bool, io::Error> {
    let mut result = true;
    let opts = compiletest::test_opts(config);
    for dir in fs::read_dir(&config.src_base)? {
        let dir = dir?;
        if !dir.file_type()?.is_dir() {
            continue;
        }
        let dir_path = dir.path();
        set_var("CARGO_MANIFEST_DIR", &dir_path);
        for file in fs::read_dir(&dir_path)? {
            let file = file?;
            let file_path = file.path();
            if !file.file_type()?.is_file() {
                continue;
            }
            if file_path.extension() != Some(OsStr::new("rs")) {
                continue;
            }
            let paths = compiletest::common::TestPaths {
                file: file_path,
                base: config.src_base.clone(),
                relative_dir: dir_path.file_name().unwrap().into(),
            };
            let test_name = compiletest::make_test_name(&config, &paths);
            let index = tests
                .iter()
                .position(|test| test.desc.name == test_name)
                .expect("The test should be in there");
            result &= test::run_tests_console(&opts, vec![tests.swap_remove(index)])?;
        }
    }
    Ok(result)
}

fn run_ui_toml() {
    let path = PathBuf::from("tests/ui-toml").canonicalize().unwrap();
    let config = config("ui", path);
    let tests = compiletest::make_tests(&config);

    let res = run_ui_toml_tests(&config, tests);
    match res {
        Ok(true) => {},
        Ok(false) => panic!("Some tests failed"),
        Err(e) => {
            println!("I/O failure during tests: {:?}", e);
        },
    }
}

fn prepare_env() {
    set_var("CLIPPY_DISABLE_DOCS_LINKS", "true");
    set_var("CLIPPY_TESTS", "true");
    //set_var("RUST_BACKTRACE", "0");
}

#[test]
fn compile_test() {
    prepare_env();
    run_mode("ui", "tests/ui".into());
    run_ui_toml();
}
<commit_msg>Re-allow clippy::identity_conversion in compiletest<commit_after>#![feature(test)]

use compiletest_rs as compiletest;
extern crate test;

use std::env::{set_var, var};
use std::ffi::OsStr;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

fn clippy_driver_path() -> PathBuf {
    if let Some(path) = option_env!("CLIPPY_DRIVER_PATH") {
        PathBuf::from(path)
    } else {
        PathBuf::from(concat!("target/", env!("PROFILE"), "/clippy-driver"))
    }
}

fn host_libs() -> PathBuf {
    if let Some(path) = option_env!("HOST_LIBS") {
        PathBuf::from(path)
    } else {
        Path::new("target").join(env!("PROFILE"))
    }
}

fn rustc_test_suite() -> Option<PathBuf> {
    option_env!("RUSTC_TEST_SUITE").map(PathBuf::from)
}

fn rustc_lib_path() -> PathBuf {
    option_env!("RUSTC_LIB_PATH").unwrap().into()
}

fn config(mode: &str, dir: PathBuf) -> compiletest::Config {
    let mut config = compiletest::Config::default();

    let cfg_mode = mode.parse().expect("Invalid mode");
    if let Ok(name) = var::<&str>("TESTNAME") {
        let s: String = name.to_owned();
        config.filter = Some(s)
    }

    if rustc_test_suite().is_some() {
        config.run_lib_path = rustc_lib_path();
        config.compile_lib_path = rustc_lib_path();
    }
    config.target_rustcflags = Some(format!(
        "-L {0} -L {0}/deps -Dwarnings -Zui-testing",
        host_libs().display()
    ));

    config.mode = cfg_mode;
    config.build_base = if rustc_test_suite().is_some() {
        // we don't need access to the stderr files on travis
        let mut path = PathBuf::from(env!("OUT_DIR"));
        path.push("test_build_base");
        path
    } else {
        let mut path = std::env::current_dir().unwrap();
        path.push("target/debug/test_build_base");
        path
    };
    config.src_base = dir;
    config.rustc_path = clippy_driver_path();
    config
}

fn run_mode(mode: &str, dir: PathBuf) {
    let cfg = config(mode, dir);
    // clean rmeta data, otherwise "cargo check; cargo test" fails (#2896)
    cfg.clean_rmeta();
    compiletest::run_tests(&cfg);
}

#[allow(clippy::identity_conversion)]
fn run_ui_toml_tests(config: &compiletest::Config, mut tests: Vec<test::TestDescAndFn>) -> Result<bool, io::Error> {
    let mut result = true;
    let opts = compiletest::test_opts(config);
    for dir in fs::read_dir(&config.src_base)? {
        let dir = dir?;
        if !dir.file_type()?.is_dir() {
            continue;
        }
        let dir_path = dir.path();
        set_var("CARGO_MANIFEST_DIR", &dir_path);
        for file in fs::read_dir(&dir_path)? {
            let file = file?;
            let file_path = file.path();
            if !file.file_type()?.is_file() {
                continue;
            }
            if file_path.extension() != Some(OsStr::new("rs")) {
                continue;
            }
            let paths = compiletest::common::TestPaths {
                file: file_path,
                base: config.src_base.clone(),
                relative_dir: dir_path.file_name().unwrap().into(),
            };
            let test_name = compiletest::make_test_name(&config, &paths);
            let index = tests
                .iter()
                .position(|test| test.desc.name == test_name)
                .expect("The test should be in there");
            result &= test::run_tests_console(&opts, vec![tests.swap_remove(index)])?;
        }
    }
    Ok(result)
}

fn run_ui_toml() {
    let path = PathBuf::from("tests/ui-toml").canonicalize().unwrap();
    let config = config("ui", path);
    let tests = compiletest::make_tests(&config);

    let res = run_ui_toml_tests(&config, tests);
    match res {
        Ok(true) => {},
        Ok(false) => panic!("Some tests failed"),
        Err(e) => {
            println!("I/O failure during tests: {:?}", e);
        },
    }
}

fn prepare_env() {
    set_var("CLIPPY_DISABLE_DOCS_LINKS", "true");
    set_var("CLIPPY_TESTS", "true");
    //set_var("RUST_BACKTRACE", "0");
}

#[test]
fn compile_test() {
    prepare_env();
    run_mode("ui", "tests/ui".into());
    run_ui_toml();
}
<|endoftext|>