use chrono::{Local, TimeZone};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use crate::session::{PLACEHOLDER_TITLE, Session};
pub(crate) const TAIL_WINDOW_INITIAL: u64 = 64 * 1024;
pub(crate) const TAIL_WINDOW_MAX: u64 = 4 * 1024 * 1024;
pub fn read_tail(path: &Path, window: u64) -> std::io::Result<(String, bool)> {
let mut f = File::open(path)?;
let len = f.metadata()?.len();
let start = len.saturating_sub(window);
f.seek(SeekFrom::Start(start))?;
let mut buf = Vec::with_capacity((len - start) as usize);
f.read_to_end(&mut buf)?;
let reached_start = start == 0;
let text = if reached_start {
String::from_utf8_lossy(&buf).into_owned()
} else {
match buf.iter().position(|&b| b == b'\n') {
Some(i) => String::from_utf8_lossy(&buf[i + 1..]).into_owned(),
None => String::new(),
}
};
Ok((text, reached_start))
}
pub(crate) fn scan_windowed(
path: &Path,
initial: u64,
max: u64,
build: impl Fn(&str, bool) -> Option<Session>,
) -> Option<Session> {
let mut window = initial;
loop {
let (text, reached_start) = read_tail(path, window).ok()?;
let mut s = build(&text, reached_start)?;
let found_title = s.title != PLACEHOLDER_TITLE;
if found_title || reached_start || window >= max {
s.message_count = None;
if s.last_activity == Local.timestamp_opt(0, 0).unwrap() {
s.last_activity = crate::util::file_mtime(path);
s.possibly_live = crate::util::is_possibly_live(s.last_activity);
}
return Some(s);
}
window = window.saturating_mul(4).min(max);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn tmp(name: &str, contents: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("ccr-tail-test-{name}"));
let mut f = std::fs::File::create(&p).unwrap();
f.write_all(contents.as_bytes()).unwrap();
p
}
fn placeholder_session(origin: &std::path::Path) -> Session {
Session {
backend: "test",
id: "id".into(),
cwd: std::path::PathBuf::from("/x"),
title: PLACEHOLDER_TITLE.into(),
last_activity: Local::now(),
message_count: Some(3),
preview: Vec::new(),
possibly_live: false,
origin: origin.to_path_buf(),
searchable: String::new(),
}
}
#[test]
fn scan_windowed_terminates_at_max_when_title_never_resolves() {
let p = tmp("scan-windowed-noresolve", &"x".repeat(500));
let calls = std::cell::Cell::new(0u32);
let s = scan_windowed(&p, 16, 64, |_text, _reached| {
calls.set(calls.get() + 1);
Some(placeholder_session(&p))
})
.expect("terminates with a session");
assert_eq!(s.title, PLACEHOLDER_TITLE);
assert_eq!(s.message_count, None); assert!((2..=5).contains(&calls.get())); std::fs::remove_file(&p).ok();
}
#[test]
fn scan_windowed_stops_when_build_resolves_title() {
let p = tmp("scan-windowed-resolve", &"x".repeat(500));
let calls = std::cell::Cell::new(0u32);
let s = scan_windowed(&p, 16, 64, |_text, _reached| {
calls.set(calls.get() + 1);
let mut sess = placeholder_session(&p);
sess.title = "resolved".into();
Some(sess)
})
.expect("session");
assert_eq!(s.title, "resolved");
assert_eq!(calls.get(), 1); std::fs::remove_file(&p).ok();
}
#[test]
fn small_file_returns_whole_and_reached_start() {
let p = tmp("small", "a\nb\nc\n");
let (text, reached) = read_tail(&p, 1024).unwrap();
assert_eq!(text, "a\nb\nc\n");
assert!(reached);
std::fs::remove_file(&p).ok();
}
#[test]
fn window_smaller_than_file_drops_partial_leading_line() {
let p = tmp("partial", "line0\nline1\nline2\nline3\nline4\n");
let (text, reached) = read_tail(&p, 12).unwrap();
assert!(!reached);
assert!(!text.contains("ine3"));
assert!(text.ends_with("line4\n"));
std::fs::remove_file(&p).ok();
}
#[test]
fn missing_file_is_err() {
let p = std::path::Path::new("/no/such/ccr/file.jsonl");
assert!(read_tail(p, 1024).is_err());
}
}