clipmem 0.4.0

macOS clipboard memory backed by SQLite and searchable from agent runtimes
Documentation
use super::*;

pub(in crate::cli) fn search(db_path: &Path, args: &SearchArgs) -> Result<()> {
    let format = args.output.resolved()?;
    let filters = normalize_retrieval_filters(&args.filters)?;
    let db = open_existing_db(db_path)?;
    let cursor = args
        .cursor
        .as_deref()
        .map(|value| parse_search_cursor(value, &args.query, args.mode, &filters))
        .transpose()?;
    let results = anyhow::Context::with_context(
        query_search_results(&db, args, &filters, cursor.as_ref()),
        || format!("search failed for query '{}'", args.query),
    )?;

    if matches!(format, OutputFormat::Text) {
        print!("{}", render_search_results_text(&results));
        return Ok(());
    }

    let projections =
        load_snapshot_projections(&db, results.hits().iter().map(SearchHit::snapshot_id))?;

    let generated_at = generated_at_now()?;
    let next_cursor = if results.has_more() {
        results
            .hits()
            .last()
            .map(|hit| {
                encode_search_cursor(&args.query, args.mode, &filters, results.mode_used(), hit)
            })
            .transpose()?
    } else {
        None
    };
    let envelope = ListEnvelope {
        schema_version: OUTPUT_SCHEMA_VERSION,
        command: "search",
        generated_at,
        applied_filters: merge_applied_filters(
            &filters,
            json!({
                "query": args.query,
                "requested_mode": args.mode.as_str(),
                "mode_used": results.mode_used().as_str(),
                "limit": args.limit,
                "cursor": args.cursor,
            }),
        ),
        truncated: results.has_more(),
        next_cursor,
        results: results
            .hits()
            .iter()
            .map(|hit| {
                let projection = projections
                    .get(&hit.snapshot_id())
                    .cloned()
                    .unwrap_or_default();
                ListRow::from_hit(hit, true, &projection)
            })
            .collect(),
    };
    emit_list_output(format, &envelope)
}

pub(in crate::cli) fn recent(db_path: &Path, args: &RecentArgs) -> Result<()> {
    let format = args.output.resolved()?;
    let filters = normalize_retrieval_filters(&args.filters)?;
    let db = open_existing_db(db_path)?;
    let cursor = args
        .cursor
        .as_deref()
        .map(|value| parse_recent_cursor(value, &filters))
        .transpose()?;
    let hits = anyhow::Context::with_context(
        db.recent_page(args.limit, &filters, cursor.as_ref()),
        || "recent query failed".to_string(),
    )?;

    if matches!(format, OutputFormat::Text) {
        print!("{}", render_hits_text(hits.items()));
        return Ok(());
    }

    let projections =
        load_snapshot_projections(&db, hits.items().iter().map(SearchHit::snapshot_id))?;

    let generated_at = generated_at_now()?;
    let next_cursor = if hits.has_more() {
        hits.items()
            .last()
            .map(|hit| encode_recent_cursor(&filters, hit))
            .transpose()?
    } else {
        None
    };
    let envelope = ListEnvelope {
        schema_version: OUTPUT_SCHEMA_VERSION,
        command: "recent",
        generated_at,
        applied_filters: merge_applied_filters(
            &filters,
            json!({
                "limit": args.limit,
                "cursor": args.cursor,
            }),
        ),
        truncated: hits.has_more(),
        next_cursor,
        results: hits
            .items()
            .iter()
            .map(|hit| {
                let projection = projections
                    .get(&hit.snapshot_id())
                    .cloned()
                    .unwrap_or_default();
                ListRow::from_hit(hit, false, &projection)
            })
            .collect(),
    };
    emit_list_output(format, &envelope)
}

pub(in crate::cli) fn timeline(db_path: &Path, args: &TimelineArgs) -> Result<()> {
    let format = args.output.resolved()?;
    let filters = normalize_retrieval_filters(&args.filters)?;
    let db = open_existing_db(db_path)?;
    let cursor = args
        .cursor
        .as_deref()
        .map(|value| parse_timeline_cursor(value, &filters, args.sort))
        .transpose()?;
    let events = anyhow::Context::with_context(
        db.timeline_page(args.limit, &filters, args.sort, cursor.as_ref()),
        || "timeline query failed".to_string(),
    )?;

    if matches!(format, OutputFormat::Text) {
        print!("{}", render_timeline_text(events.items()));
        return Ok(());
    }

    let projections =
        load_snapshot_projections(&db, events.items().iter().map(TimelineEvent::snapshot_id))?;

    let next_cursor = if events.has_more() {
        events
            .items()
            .last()
            .map(|event| encode_timeline_cursor(&filters, args.sort, event))
            .transpose()?
    } else {
        None
    };
    let envelope = ListEnvelope {
        schema_version: OUTPUT_SCHEMA_VERSION,
        command: "timeline",
        generated_at: generated_at_now()?,
        applied_filters: merge_applied_filters(
            &filters,
            json!({
                "limit": args.limit,
                "sort": args.sort.as_str(),
                "cursor": args.cursor,
            }),
        ),
        truncated: events.has_more(),
        next_cursor,
        results: events
            .items()
            .iter()
            .map(|event| {
                let projection = projections
                    .get(&event.snapshot_id())
                    .cloned()
                    .unwrap_or_default();
                ListRow::from_timeline_event(event, &projection)
            })
            .collect(),
    };
    emit_list_output(format, &envelope)
}

pub(in crate::cli) fn stats(db_path: &Path, args: &StatsArgs) -> Result<()> {
    let format = args.output.resolved()?;
    let filters = normalize_retrieval_filters(&args.filters)?;
    let db = open_existing_db(db_path)?;
    let report = anyhow::Context::context(db.stats(&filters), "stats query failed")?;

    if matches!(format, StatsOutputFormat::Text) {
        print!("{}", render_stats_text(&report));
        return Ok(());
    }

    let envelope = StatsEnvelope {
        schema_version: OUTPUT_SCHEMA_VERSION,
        command: "stats",
        generated_at: generated_at_now()?,
        applied_filters: serde_json::to_value(&filters)?,
        stats: report,
    };
    if matches!(format, StatsOutputFormat::Human) {
        print!("{}", render_stats_human(&envelope));
        Ok(())
    } else {
        emit_json_or_text(true, &envelope, |_| String::new())
    }
}

pub(in crate::cli) fn show_snapshot(db_path: &Path, args: &GetArgs) -> Result<()> {
    let format = args.output.resolved()?;
    let filters = normalize_retrieval_filters(&args.filters)?;
    let db = open_existing_db(db_path)?;
    let snapshot =
        anyhow::Context::with_context(db.find_snapshot(args.snapshot_id, args.events), || {
            format!("get failed for snapshot {}", args.snapshot_id)
        })?
        .ok_or_else(|| anyhow!("snapshot {} was not found", args.snapshot_id))?;
    if !db.snapshot_matches_filters(args.snapshot_id, &filters)? {
        return Err(anyhow!(
            "snapshot {} does not satisfy the active filters",
            args.snapshot_id
        ));
    }

    if matches!(format, OutputFormat::Text) {
        print!("{}", render_snapshot_text(&snapshot));
        return Ok(());
    }

    let envelope = GetEnvelope {
        schema_version: OUTPUT_SCHEMA_VERSION,
        command: "get",
        generated_at: generated_at_now()?,
        applied_filters: serde_json::to_value(&filters)?,
        snapshot,
    };
    emit_get_output(format, &envelope)
}