#![allow(non_snake_case)]
#![allow(dead_code)]
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::os::unix::fs::OpenOptionsExt;
use crate::ported::lineeditor::{LineEditor, LineEditor_getText};
const HISTORY_MAX_ENTRIES: usize = 512;
const LINEEDITOR_MAX: usize = 128;
pub struct History {
pub entries: Vec<String>,
pub count: usize,
pub capacity: usize,
pub position: usize,
pub saved: String,
pub filename: Option<String>,
}
pub fn History_load(this: &mut History) {
let filename = match &this.filename {
Some(f) => f.clone(),
None => return,
};
let file = match File::open(&filename) {
Ok(f) => f,
Err(_) => return,
};
let reader = BufReader::new(file);
for line in reader.lines() {
let line = match line {
Ok(l) => l,
Err(_) => break,
};
let line = line.trim_end_matches(|c| c == '\n' || c == '\r');
if line.is_empty() {
continue;
}
History_add(this, line);
}
}
pub fn History_new(filename: Option<&str>) -> History {
let mut this = History {
entries: Vec::with_capacity(64),
count: 0,
capacity: 64,
position: 0,
saved: String::new(),
filename: filename.map(|s| s.to_string()),
};
if this.filename.is_some() {
History_load(&mut this);
}
this.position = this.count;
this
}
pub fn History_delete(this: History) {
let _ = this;
}
pub fn History_save(this: &History) {
let filename = match &this.filename {
Some(f) => f,
None => return,
};
let file = match OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(filename)
{
Ok(f) => f,
Err(_) => return,
};
let mut fp = BufWriter::new(file);
let start = if this.count > HISTORY_MAX_ENTRIES {
this.count - HISTORY_MAX_ENTRIES
} else {
0
};
for i in start..this.count {
let _ = writeln!(fp, "{}", this.entries[i]);
}
let _ = fp.flush();
}
pub fn History_add(this: &mut History, entry: &str) {
if entry.is_empty() {
return;
}
for i in 0..this.count {
if this.entries[i] == entry {
this.entries.remove(i);
this.count -= 1;
break;
}
}
if this.count >= this.capacity {
if this.capacity < HISTORY_MAX_ENTRIES {
this.capacity = (this.capacity * 2).min(HISTORY_MAX_ENTRIES);
} else {
this.entries.remove(0);
this.count -= 1;
}
}
this.entries.push(entry.to_string());
this.count += 1;
this.position = this.count;
this.saved.clear();
}
pub fn History_navigate<'a>(
this: &'a mut History,
editor: &LineEditor,
back: bool,
) -> Option<&'a str> {
if this.count == 0 {
return None;
}
if back {
if this.position == this.count {
let text = LineEditor_getText(editor);
let mut end = text.len().min(LINEEDITOR_MAX);
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
this.saved.clear();
this.saved.push_str(&text[..end]);
}
if this.position > 0 {
this.position -= 1;
return Some(&this.entries[this.position]);
}
None } else {
if this.position >= this.count {
return None; }
this.position += 1;
if this.position == this.count {
return Some(&this.saved);
}
Some(&this.entries[this.position])
}
}
pub fn History_resetPosition(this: &mut History) {
this.position = this.count;
this.saved.clear();
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
fn temp_path(tag: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let mut p = std::env::temp_dir();
p.push(format!(
"htoprs_history_{}_{}_{}_{}",
std::process::id(),
tag,
nanos,
n
));
p
}
#[test]
fn new_none_defaults() {
let h = History_new(None);
assert!(h.entries.is_empty());
assert_eq!(h.count, 0);
assert_eq!(h.capacity, 64);
assert_eq!(h.position, 0);
assert!(h.saved.is_empty());
assert!(h.filename.is_none());
}
#[test]
fn add_appends_and_parks_position() {
let mut h = History_new(None);
h.position = 0; h.saved.push_str("in progress");
History_add(&mut h, "one");
History_add(&mut h, "two");
assert_eq!(h.entries, vec!["one".to_string(), "two".to_string()]);
assert_eq!(h.count, 2);
assert_eq!(h.position, 2); assert!(h.saved.is_empty()); }
#[test]
fn add_ignores_empty_entry() {
let mut h = History_new(None);
History_add(&mut h, "");
assert_eq!(h.count, 0);
assert!(h.entries.is_empty());
}
#[test]
fn add_dedups_and_moves_to_end() {
let mut h = History_new(None);
History_add(&mut h, "a");
History_add(&mut h, "b");
History_add(&mut h, "c");
History_add(&mut h, "a");
assert_eq!(
h.entries,
vec!["b".to_string(), "c".to_string(), "a".to_string()]
);
assert_eq!(h.count, 3);
}
#[test]
fn add_dedup_of_immediate_repeat() {
let mut h = History_new(None);
History_add(&mut h, "same");
History_add(&mut h, "same");
assert_eq!(h.entries, vec!["same".to_string()]);
assert_eq!(h.count, 1);
}
#[test]
fn add_rotates_dropping_oldest_at_cap() {
let mut h = History_new(None);
let total = HISTORY_MAX_ENTRIES + 88; for i in 0..total {
History_add(&mut h, &format!("e{}", i));
}
assert_eq!(h.count, HISTORY_MAX_ENTRIES);
assert_eq!(h.entries.len(), HISTORY_MAX_ENTRIES);
let oldest = total - HISTORY_MAX_ENTRIES; assert_eq!(h.entries.first().unwrap(), &format!("e{}", oldest));
assert_eq!(h.entries.last().unwrap(), &format!("e{}", total - 1));
assert_eq!(h.position, HISTORY_MAX_ENTRIES);
}
#[test]
fn capacity_doubles_up_to_cap() {
let mut h = History_new(None);
assert_eq!(h.capacity, 64);
for i in 0..65 {
History_add(&mut h, &format!("e{}", i));
}
assert_eq!(h.capacity, 128);
for i in 65..300 {
History_add(&mut h, &format!("e{}", i));
}
assert_eq!(h.capacity, HISTORY_MAX_ENTRIES);
}
#[test]
fn reset_position_parks_and_clears() {
let mut h = History_new(None);
History_add(&mut h, "a");
History_add(&mut h, "b");
h.position = 0;
h.saved.push_str("browsing");
History_resetPosition(&mut h);
assert_eq!(h.position, h.count);
assert_eq!(h.position, 2);
assert!(h.saved.is_empty());
}
#[test]
fn load_missing_file_is_noop() {
let path = temp_path("missing");
let _ = fs::remove_file(&path);
let h = History_new(Some(path.to_str().unwrap()));
assert_eq!(h.count, 0);
assert!(h.entries.is_empty());
}
#[test]
fn load_strips_newlines_skips_blanks_and_dedups() {
let path = temp_path("load");
fs::write(&path, "a\n\nb\r\na\n").unwrap();
let h = History_new(Some(path.to_str().unwrap()));
assert_eq!(h.entries, vec!["b".to_string(), "a".to_string()]);
assert_eq!(h.count, 2);
assert_eq!(h.position, 2);
let _ = fs::remove_file(&path);
}
#[test]
fn save_writes_one_entry_per_line() {
let path = temp_path("save");
let mut h = History_new(Some(path.to_str().unwrap()));
History_add(&mut h, "first");
History_add(&mut h, "second");
History_save(&h);
let content = fs::read_to_string(&path).unwrap();
assert_eq!(content, "first\nsecond\n");
let _ = fs::remove_file(&path);
}
#[test]
fn save_then_load_roundtrip() {
let save_path = temp_path("rt_save");
let mut h = History_new(Some(save_path.to_str().unwrap()));
History_add(&mut h, "alpha");
History_add(&mut h, "beta");
History_add(&mut h, "gamma");
History_save(&h);
let reloaded = History_new(Some(save_path.to_str().unwrap()));
assert_eq!(
reloaded.entries,
vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()]
);
assert_eq!(reloaded.count, 3);
let _ = fs::remove_file(&save_path);
}
#[test]
fn navigate_empty_ring_returns_none() {
use crate::ported::lineeditor::{LineEditor, LineEditor_init};
let mut h = History_new(None);
let mut e = LineEditor::default();
LineEditor_init(&mut e);
assert_eq!(History_navigate(&mut h, &e, true), None);
assert_eq!(History_navigate(&mut h, &e, false), None);
}
#[test]
fn navigate_back_and_forward_walks_and_restores_saved() {
use crate::ported::lineeditor::{LineEditor, LineEditor_init, LineEditor_setText};
let mut h = History_new(None);
History_add(&mut h, "one");
History_add(&mut h, "two");
History_add(&mut h, "three");
assert_eq!(h.position, 3);
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "in-progress");
assert_eq!(History_navigate(&mut h, &e, true), Some("three"));
assert_eq!(h.saved, "in-progress");
assert_eq!(History_navigate(&mut h, &e, true), Some("two"));
assert_eq!(History_navigate(&mut h, &e, true), Some("one"));
assert_eq!(History_navigate(&mut h, &e, true), None);
assert_eq!(h.position, 0);
assert_eq!(History_navigate(&mut h, &e, false), Some("two"));
assert_eq!(History_navigate(&mut h, &e, false), Some("three"));
assert_eq!(History_navigate(&mut h, &e, false), Some("in-progress"));
assert_eq!(h.position, 3);
assert_eq!(History_navigate(&mut h, &e, false), None);
}
#[test]
fn navigate_saved_only_captured_on_first_step_back() {
use crate::ported::lineeditor::{LineEditor, LineEditor_init, LineEditor_setText};
let mut h = History_new(None);
History_add(&mut h, "a");
History_add(&mut h, "b");
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "first");
assert_eq!(History_navigate(&mut h, &e, true), Some("b"));
assert_eq!(h.saved, "first");
LineEditor_setText(&mut e, "changed");
assert_eq!(History_navigate(&mut h, &e, true), Some("a"));
assert_eq!(h.saved, "first");
}
#[test]
fn save_none_filename_is_noop() {
let mut h = History_new(None);
History_add(&mut h, "x");
History_save(&h); assert_eq!(h.count, 1);
}
}