readchar 0.1.1

a simple to use calculator
Documentation
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::Read;
use std::io::Write;
use std::path::Path;
#[derive(Default, Debug)]
pub enum History {
    Local(LocalHistory),
    File(LocalHistory, File),
    #[default]
    None,
}
impl History {
    /// creates a new history struct either using local history, specified by None
    ///
    /// or having local history and writing new entries to the file specified by path,
    /// notably does not read new entries of the path after this initialization
    ///
    /// use History::default() for no history
    pub fn new(path: Option<&Path>) -> io::Result<Self> {
        if let Some(path) = path {
            let mut file = OpenOptions::new()
                .read(true)
                .append(true)
                .create(true)
                .open(path)?;
            let mut history = String::with_capacity(file.metadata()?.len() as usize);
            file.read_to_string(&mut history)?;
            let char_index = history.len();
            let lines = history.lines().count();
            Ok(Self::File(
                LocalHistory {
                    history,
                    history_modified: HashMap::with_capacity(8),
                    char_index,
                    index: lines,
                    lines,
                },
                file,
            ))
        } else {
            Ok(Self::Local(LocalHistory {
                history: String::with_capacity(256),
                history_modified: HashMap::with_capacity(8),
                char_index: 0,
                index: 0,
                lines: 0,
            }))
        }
    }
    pub(crate) fn mv(&mut self, cur: &str, up: bool) -> &str {
        match self {
            History::Local(v) | History::File(v, _) => v.mv(cur, up),
            History::None => unreachable!(),
        }
    }
    pub(crate) fn at_start(&self) -> bool {
        match self {
            History::Local(h) | History::File(h, _) => h.index == 0,
            History::None => true,
        }
    }
    pub(crate) fn at_end(&self) -> bool {
        match self {
            History::Local(h) | History::File(h, _) => h.index == h.lines,
            History::None => true,
        }
    }
    pub(crate) fn push(&mut self, s: &str) -> io::Result<()> {
        match self {
            History::Local(h) => h.push(s),
            History::File(h, f) => {
                h.push(s);
                writeln!(f, "{s}")?;
            }
            History::None => {}
        }
        Ok(())
    }
}
#[derive(Debug)]
pub struct LocalHistory {
    history: String,
    history_modified: HashMap<usize, String>,
    char_index: usize,
    index: usize,
    lines: usize,
}
impl LocalHistory {
    pub(crate) fn history_modified(&self, cur: &str) -> bool {
        self.history_modified
            .get(&self.index)
            .map(|s| s != cur)
            .unwrap_or(
                self.index == self.lines
                    || self
                        .history
                        .get(self.char_index + 1..self.char_index + 1 + cur.len())
                        .map(|s| s != cur)
                        .unwrap_or(true)
                    || self
                        .history
                        .as_bytes()
                        .get(self.char_index + 1 + cur.len())
                        .map(|c| *c != b'\n')
                        .unwrap_or(false),
            )
    }
    pub(crate) fn mv(&mut self, cur: &str, up: bool) -> &str {
        if up {
            if self.history_modified(cur) {
                self.history_modified.insert(self.index, cur.to_string());
            }
            self.index -= 1;
            let last = self.history[..self.char_index]
                .rfind('\n')
                .map(|i| i + 1)
                .unwrap_or(0);
            let s = &self.history[last..self.char_index];
            self.char_index = last.saturating_sub(1);
            self.history_modified
                .get(&self.index)
                .map(|s| s.as_str())
                .unwrap_or(s)
        } else if self.index + 1 == self.lines {
            self.index += 1;
            self.char_index = self.history.len() - 1;
            &self.history_modified[&self.index]
        } else {
            if self.history_modified(cur) {
                self.history_modified.insert(self.index, cur.to_string());
            }
            self.index += 1;
            let next = self.history[self.char_index + 1..]
                .find('\n')
                .map(|i| self.char_index + 1 + i)
                .unwrap();
            let f = self.history[next + 1..]
                .find('\n')
                .map(|i| next + 1 + i)
                .unwrap_or(self.history.len());
            let s = &self.history[next + 1..f];
            self.char_index = next;
            self.history_modified
                .get(&self.index)
                .map(|s| s.as_str())
                .unwrap_or(s)
        }
    }
    pub(crate) fn push(&mut self, s: &str) {
        use std::fmt::Write;
        writeln!(&mut self.history, "{s}").unwrap();
        self.history_modified.clear();
        self.char_index = self.history.len() - 1;
        self.lines += 1;
        self.index = self.lines;
    }
}