qem 0.6.2

High-performance cross-platform text engine for massive files.
Documentation
use qem::{DocumentSession, LoadPhase, TextPosition, ViewportRequest};
use std::env;
use std::path::PathBuf;
use std::thread;
use std::time::{Duration, Instant};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut args = env::args_os().skip(1);
    let input = args.next().map(PathBuf::from).ok_or(
        "usage: cargo run --example frontend_session --features editor -- <input> [output]",
    )?;
    let output = args.next().map(PathBuf::from);

    let mut session = DocumentSession::new();
    session.open_file_async(input.clone())?;
    wait_for_load(&mut session, Duration::from_secs(5))?;

    let viewport = session.read_viewport(ViewportRequest::new(0, 20).with_columns(0, 160));
    let status = session.status();

    println!("opened: {}", input.display());
    println!("generation: {}", status.generation());
    println!("bytes: {}", status.file_len());
    println!("backing: {}", status.backing().as_str());
    println!(
        "lines: {} ({})",
        status.display_line_count(),
        if status.is_line_count_exact() {
            "exact"
        } else {
            "estimated"
        }
    );
    println!("dirty: {}", status.is_dirty());
    println!("line ending: {:?}", status.line_ending());
    println!(
        "edit capability at line 1: {:?}",
        session.edit_capability_at(TextPosition::new(0, 0))
    );
    println!("viewport:");
    for row in viewport.rows() {
        println!(
            "{:>8}: [{}] {}",
            row.line_number(),
            if row.is_exact() { "=" } else { "~" },
            row.text()
        );
    }

    if let Some(output) = output {
        let _ = session.try_insert(
            TextPosition::new(0, 0),
            "// saved by qem frontend_session example\n",
        )?;
        let maintenance = session.maintenance_status();
        if let Some(stats) = maintenance.fragmentation_stats() {
            println!(
                "fragmentation: pieces={}, avg_bytes={:.1}, small_ratio={:.3}",
                stats.piece_count(),
                stats.average_piece_bytes(),
                stats.fragmentation_ratio()
            );
        }
        println!(
            "maintenance action: {}",
            maintenance.recommended_action().as_str()
        );
        if let Some(recommendation) = maintenance.compaction_recommendation() {
            println!("compaction recommended: {:?}", recommendation.urgency());
        }
        let idle_outcome = session.run_idle_compaction()?;
        if idle_outcome.is_compacted() {
            println!("idle maintenance compacted the piece table");
        } else if idle_outcome.is_forced_pending() {
            println!("idle maintenance left a forced compaction pending for save/operator action");
        }

        if session.save_as_async(output.clone())? {
            wait_for_save(&mut session, Duration::from_secs(5))?;
        }

        println!("saved copy: {}", output.display());
        println!("dirty after save: {}", session.is_dirty());
    }

    Ok(())
}

fn wait_for_load(
    session: &mut DocumentSession,
    timeout: Duration,
) -> Result<(), Box<dyn std::error::Error>> {
    let deadline = Instant::now() + timeout;
    let mut last_progress = None;

    loop {
        let status = session.status();
        if let Some(progress) = status.loading_state() {
            let progress_key = (
                progress.completed_bytes(),
                progress.total_bytes(),
                status.loading_phase(),
            );
            if last_progress != Some(progress_key) {
                println!(
                    "loading: {}/{} bytes from {}, phase: {:?}",
                    progress.completed_bytes(),
                    progress.total_bytes(),
                    progress.path().display(),
                    status.loading_phase().unwrap_or(LoadPhase::Opening)
                );
                last_progress = Some(progress_key);
            }
        }

        if let Some(result) = session.poll_load_job() {
            result?;
            wait_for_indexing(session, Duration::from_millis(500));
            return Ok(());
        }

        if !session.is_loading() {
            wait_for_indexing(session, Duration::from_millis(500));
            return Ok(());
        }

        if Instant::now() >= deadline {
            return Err("background load timed out".into());
        }

        thread::sleep(Duration::from_millis(10));
    }
}

fn wait_for_save(
    session: &mut DocumentSession,
    timeout: Duration,
) -> Result<(), Box<dyn std::error::Error>> {
    let deadline = Instant::now() + timeout;
    let mut last_progress = None;

    loop {
        let status = session.status();
        if let Some(progress) = status.save_state() {
            let progress_key = (progress.completed_bytes(), progress.total_bytes());
            if last_progress != Some(progress_key) {
                println!(
                    "saving: {}/{} bytes to {}",
                    progress.completed_bytes(),
                    progress.total_bytes(),
                    progress.path().display()
                );
                last_progress = Some(progress_key);
            }
        }

        if let Some(result) = session.poll_save_job() {
            result?;
            return Ok(());
        }

        if !session.is_saving() {
            return Ok(());
        }

        if Instant::now() >= deadline {
            return Err("background save timed out".into());
        }

        thread::sleep(Duration::from_millis(10));
    }
}

fn wait_for_indexing(session: &DocumentSession, timeout: Duration) {
    let deadline = Instant::now() + timeout;
    let mut last_progress = None;

    while Instant::now() < deadline {
        let status = session.status();
        if !status.is_indexing() {
            break;
        }
        if let Some(progress) = status.indexing_state() {
            if last_progress != Some(progress) {
                println!(
                    "indexing: {}/{} bytes",
                    progress.completed_bytes(),
                    progress.total_bytes()
                );
                last_progress = Some(progress);
            }
        }

        thread::sleep(Duration::from_millis(10));
    }
}