blerust 0.1.16

Blazing fast and robust line editor in Rust (replacement for blesh)
Documentation
use std::env;
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};

pub struct History {
    entries: Vec<String>,
    cursor: Option<usize>,
    file_path: Option<PathBuf>,
    max_entries: usize,
    pub search_prefix: Option<String>,
}

impl Default for History {
    fn default() -> Self {
        Self::new()
    }
}

impl History {
    pub fn new() -> Self {
        let mut history = Self {
            entries: Vec::new(),
            cursor: None,
            file_path: None,
            max_entries: 10000,
            search_prefix: None,
        };

        if let Some(home) = env::var_os("HOME") {
            let path = PathBuf::from(home).join(".bash_history");
            let _ = history.load_from_file(&path);
            history.file_path = Some(path);
        }

        history
    }

    pub fn with_file<P: AsRef<Path>>(path: P) -> Self {
        let mut history = Self {
            entries: Vec::new(),
            cursor: None,
            file_path: Some(path.as_ref().to_path_buf()),
            max_entries: 10000,
            search_prefix: None,
        };
        let _ = history.load_from_file(path);
        history
    }

    pub fn load_from_file<P: AsRef<Path>>(&mut self, path: P) -> std::io::Result<()> {
        if !path.as_ref().exists() {
            return Ok(());
        }
        let file = File::open(path)?;
        let reader = BufReader::new(file);
        for line in reader.lines() {
            let line = line?;
            let trimmed = line.trim();
            if !trimmed.is_empty() {
                self.entries.push(trimmed.to_string());
            }
        }
        if self.entries.len() > self.max_entries {
            let drop_count = self.entries.len() - self.max_entries;
            self.entries.drain(0..drop_count);
        }
        Ok(())
    }

    pub fn add(&mut self, entry: &str) {
        let trimmed = entry.trim();
        if trimmed.is_empty() {
            return;
        }

        if self.entries.last().map(|s| s.as_str()) == Some(trimmed) {
            return;
        }

        self.entries.push(trimmed.to_string());
        if self.entries.len() > self.max_entries {
            self.entries.remove(0);
        }

        if let Some(ref path) = self.file_path
            && let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path)
        {
            let _ = writeln!(file, "{}", trimmed);
        }

        self.reset_cursor();
    }

    pub fn reset_cursor(&mut self) {
        self.cursor = None;
        self.search_prefix = None;
    }

    pub fn suggest_suffix(&self, prefix: &str) -> Option<String> {
        if prefix.is_empty() {
            return None;
        }
        for entry in self.entries.iter().rev() {
            if entry.starts_with(prefix) && entry.len() > prefix.len() {
                return Some(entry[prefix.len()..].to_string());
            }
        }
        None
    }

    pub fn previous_match(&mut self, prefix: &str) -> Option<&str> {
        let len = self.entries.len();
        if len == 0 {
            return None;
        }

        let start_idx = match self.cursor {
            Some(idx) => {
                if idx == 0 {
                    0
                } else {
                    idx - 1
                }
            }
            None => len - 1,
        };

        for i in (0..=start_idx).rev() {
            if self.entries[i].starts_with(prefix) {
                self.cursor = Some(i);
                return Some(&self.entries[i]);
            }
        }

        None
    }

    pub fn next_match(&mut self, prefix: &str) -> Option<&str> {
        let len = self.entries.len();
        if len == 0 {
            return None;
        }

        let start_idx = self.cursor? + 1;

        for i in start_idx..len {
            if self.entries[i].starts_with(prefix) {
                self.cursor = Some(i);
                return Some(&self.entries[i]);
            }
        }

        self.cursor = None;
        None
    }

    pub fn len(&self) -> usize {
        self.entries.len()
    }

    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }
}