Skip to content
Snippets Groups Projects
history.rs 9.56 KiB
Newer Older
//! History API

use std::collections::VecDeque;
gwenn's avatar
gwenn committed
use std::collections::vec_deque;
use std::fs::File;
gwenn's avatar
gwenn committed
use std::iter::DoubleEndedIterator;
use std::ops::Index;
use std::path::Path;
gwenn's avatar
gwenn committed
#[cfg(unix)]
use libc;

use super::Result;
gwenn's avatar
gwenn committed
use config::{Config, HistoryDuplicates};
gwenn's avatar
gwenn committed
/// Search direction
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Direction {
    Forward,
    Reverse,
}

gwenn's avatar
gwenn committed
/// Current state of the history.
gwenn's avatar
gwenn committed
#[derive(Default)]
pub struct History {
    entries: VecDeque<String>,
    max_len: usize,
    ignore_space: bool,
    ignore_dups: bool,
}

impl History {
    pub fn new() -> History {
        Self::with_config(Config::default())
    }
    pub fn with_config(config: Config) -> History {
gwenn's avatar
gwenn committed
        History {
            entries: VecDeque::new(),
gwenn's avatar
gwenn committed
            max_len: config.max_history_size(),
            ignore_space: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive,
            ignore_dups: config.history_ignore_space(),
gwenn's avatar
gwenn committed
        }
    /// Return the history entry at position `index`, starting from 0.
gwenn's avatar
gwenn committed
    pub fn get(&self, index: usize) -> Option<&String> {
gwenn's avatar
gwenn committed
        self.entries.get(index)
gwenn's avatar
gwenn committed
    /// Return the last history entry (i.e. previous command)
    pub fn last(&self) -> Option<&String> {
        self.entries.back()
    }

    /// Add a new entry in the history.
gwenn's avatar
gwenn committed
    pub fn add<S: AsRef<str> + Into<String>>(&mut self, line: S) -> bool {
        if self.max_len == 0 {
            return false;
        }
gwenn's avatar
gwenn committed
        if line.as_ref().is_empty() ||
gwenn's avatar
gwenn committed
            (self.ignore_space &&
gwenn's avatar
gwenn committed
                line.as_ref()
                    .chars()
                    .next()
                    .map_or(true, |c| c.is_whitespace()))
gwenn's avatar
gwenn committed
        {
            return false;
        }
        if self.ignore_dups {
            if let Some(s) = self.entries.back() {
gwenn's avatar
gwenn committed
                if s == line.as_ref() {
        }
        if self.entries.len() == self.max_len {
            self.entries.pop_front();
        }
gwenn's avatar
gwenn committed
        self.entries.push_back(line.into());
gwenn's avatar
gwenn committed
        true
gwenn's avatar
gwenn committed
    /// Return the number of entries in the history.
    pub fn len(&self) -> usize {
        self.entries.len()
    }
gwenn's avatar
gwenn committed
    /// Return true if the history has no entry.
gwenn's avatar
gwenn committed
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    /// Set the maximum length for the history. This function can be called even
    /// if there is already some history, the function will make sure to retain
gwenn's avatar
gwenn committed
    /// just the latest `len` elements if the new history length value is
gwenn's avatar
gwenn committed
    /// smaller than the amount of items already inside the history.
    ///
gwenn's avatar
gwenn committed
    /// Like [stifle_history](http://cnswww.cns.cwru.
    /// edu/php/chet/readline/history.html#IDX11).
    pub fn set_max_len(&mut self, len: usize) {
        self.max_len = len;
        if len == 0 {
            self.entries.clear();
            return;
        }
        loop {
            if self.entries.len() <= len {
                break;
            }
            self.entries.pop_front();
        }
    }

    /// Save the history in the specified file.
gwenn's avatar
gwenn committed
    // TODO append_history
    // http://cnswww.cns.cwru.edu/php/chet/readline/history.html#IDX30
    // TODO history_truncate_file
    // http://cnswww.cns.cwru.edu/php/chet/readline/history.html#IDX31
gwenn's avatar
gwenn committed
    pub fn save<P: AsRef<Path> + ?Sized>(&self, path: &P) -> Result<()> {
        use std::io::{BufWriter, Write};

gwenn's avatar
gwenn committed
        if self.is_empty() {
            return Ok(());
        }
gwenn's avatar
gwenn committed
        let old_umask = umask();
        let f = File::create(path);
gwenn's avatar
gwenn committed
        restore_umask(old_umask);
        let file = try!(f);
gwenn's avatar
gwenn committed
        fix_perm(&file);
        let mut wtr = BufWriter::new(file);
gwenn's avatar
gwenn committed
        for entry in &self.entries {
            try!(wtr.write_all(entry.as_bytes()));
            try!(wtr.write_all(b"\n"));
        }
gwenn's avatar
gwenn committed
        Ok(())
    }

    /// Load the history from the specified file.
gwenn's avatar
gwenn committed
    ///
gwenn's avatar
gwenn committed
    /// # Errors
    /// Will return `Err` if path does not already exist or could not be read.
gwenn's avatar
gwenn committed
    pub fn load<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> {
        use std::io::{BufRead, BufReader};

        let file = try!(File::open(&path));
        let rdr = BufReader::new(file);
        for line in rdr.lines() {
            self.add(try!(line).as_ref()); // TODO truncate to MAX_LINE
        }
gwenn's avatar
gwenn committed
        Ok(())
Gwenael Treguier's avatar
Gwenael Treguier committed

    /// Clear history
    pub fn clear(&mut self) {
        self.entries.clear()
    }
gwenn's avatar
gwenn committed
    /// Search history (start position inclusive [0, len-1]).
gwenn's avatar
gwenn committed
    ///
gwenn's avatar
gwenn committed
    /// Return the absolute index of the nearest history entry that matches
    /// `term`.
gwenn's avatar
gwenn committed
    ///
gwenn's avatar
gwenn committed
    /// Return None if no entry contains `term` between [start, len -1] for
    /// forward search
gwenn's avatar
gwenn committed
    /// or between [0, start] for reverse search.
    pub fn search(&self, term: &str, start: usize, dir: Direction) -> Option<usize> {
        let test = |entry: &String| entry.contains(term);
        self.search_match(term, start, dir, test)
    }

gwenn's avatar
gwenn committed
    /// Anchored search
    pub fn starts_with(&self, term: &str, start: usize, dir: Direction) -> Option<usize> {
        let test = |entry: &String| entry.starts_with(term);
        self.search_match(term, start, dir, test)
    }

    fn search_match<F>(&self, term: &str, start: usize, dir: Direction, test: F) -> Option<usize>
gwenn's avatar
gwenn committed
    where
        F: Fn(&String) -> bool,
        if term.is_empty() || start >= self.len() {
            return None;
        }
        match dir {
            Direction::Reverse => {
                let index = self.entries
                    .iter()
                    .rev()
                    .skip(self.entries.len() - 1 - start)
                    .position(test);
                index.and_then(|index| Some(start - index))
            }
            Direction::Forward => {
                let index = self.entries.iter().skip(start).position(test);
                index.and_then(|index| Some(index + start))
            }
gwenn's avatar
gwenn committed

    /// Return a forward iterator.
    pub fn iter(&self) -> Iter {
        Iter(self.entries.iter())
    }
}

impl Index<usize> for History {
    type Output = String;

    fn index(&self, index: usize) -> &String {
        &self.entries[index]
    }
}

impl<'a> IntoIterator for &'a History {
    type Item = &'a String;
    type IntoIter = Iter<'a>;

    fn into_iter(self) -> Iter<'a> {
        self.iter()
    }
}

/// History iterator.
pub struct Iter<'a>(vec_deque::Iter<'a, String>);

impl<'a> Iterator for Iter<'a> {
    type Item = &'a String;

    fn next(&mut self) -> Option<&'a String> {
        self.0.next()
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        self.0.size_hint()
    }
}

impl<'a> DoubleEndedIterator for Iter<'a> {
    fn next_back(&mut self) -> Option<&'a String> {
        self.0.next_back()
    }
gwenn's avatar
gwenn committed
#[cfg(windows)]
fn umask() -> u16 {
    0
}
#[cfg(unix)]
gwenn's avatar
gwenn committed
fn umask() -> libc::mode_t {
gwenn's avatar
gwenn committed
    unsafe { libc::umask(libc::S_IXUSR | libc::S_IRWXG | libc::S_IRWXO) }
}
#[cfg(windows)]
fn restore_umask(_: u16) {}
#[cfg(unix)]
gwenn's avatar
gwenn committed
fn restore_umask(old_umask: libc::mode_t) {
gwenn's avatar
gwenn committed
    unsafe {
        libc::umask(old_umask);
    }
}

#[cfg(windows)]
gwenn's avatar
gwenn committed
fn fix_perm(_: &File) {}
gwenn's avatar
gwenn committed
#[cfg(unix)]
fn fix_perm(file: &File) {
    use std::os::unix::io::AsRawFd;
    unsafe {
        libc::fchmod(file.as_raw_fd(), libc::S_IRUSR | libc::S_IWUSR);
    }
}

#[cfg(test)]
mod tests {
    extern crate tempdir;
    use std::path::Path;
gwenn's avatar
gwenn committed
    use config::Config;
    use super::{Direction, History};
    fn init() -> History {
        let mut history = History::new();
        assert!(history.add("line1"));
        assert!(history.add("line2"));
        assert!(history.add("line3"));
gwenn's avatar
gwenn committed
        history
    }

    #[test]
    fn new() {
        let history = History::new();
        assert_eq!(0, history.entries.len());
    }

    #[test]
    fn add() {
        let config = Config::builder().history_ignore_space(true).build();
        let mut history = History::with_config(config);
        assert_eq!(config.max_history_size(), history.max_len);
        assert!(history.add("line1"));
        assert!(history.add("line2"));
gwenn's avatar
gwenn committed
        assert!(!history.add("line2"));
        assert!(!history.add(""));
        assert!(!history.add(" line3"));
    }

    #[test]
    fn set_max_len() {
        let mut history = init();
        history.set_max_len(1);
        assert_eq!(1, history.entries.len());
        assert_eq!(Some(&"line3".to_owned()), history.last());
    }

    #[test]
    fn save() {
        let mut history = init();
        let td = tempdir::TempDir::new_in(&Path::new("."), "histo").unwrap();
        let history_path = td.path().join(".history");

        history.save(&history_path).unwrap();
        history.load(&history_path).unwrap();
        td.close().unwrap();
    }

    #[test]
    fn search() {
        let history = init();
        assert_eq!(None, history.search("", 0, Direction::Forward));
        assert_eq!(None, history.search("none", 0, Direction::Forward));
        assert_eq!(None, history.search("line", 3, Direction::Forward));
        assert_eq!(Some(0), history.search("line", 0, Direction::Forward));
        assert_eq!(Some(1), history.search("line", 1, Direction::Forward));
        assert_eq!(Some(2), history.search("line3", 1, Direction::Forward));
    }

    #[test]
    fn reverse_search() {
        let history = init();
        assert_eq!(None, history.search("", 2, Direction::Reverse));
        assert_eq!(None, history.search("none", 2, Direction::Reverse));
        assert_eq!(None, history.search("line", 3, Direction::Reverse));
        assert_eq!(Some(2), history.search("line", 2, Direction::Reverse));
        assert_eq!(Some(1), history.search("line", 1, Direction::Reverse));
        assert_eq!(Some(0), history.search("line1", 1, Direction::Reverse));
gwenn's avatar
gwenn committed
}