clipmem 0.4.0

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

pub(in crate::cli) fn parse_search_cursor(
    encoded: &str,
    query: &str,
    requested_mode: SearchMode,
    filters: &RetrievalFilters,
) -> Result<SearchCursorState> {
    let token: SearchCursorToken = decode_cursor(encoded)?;
    if token.command != "search" {
        return Err(anyhow!(
            "cursor is for command `{}` but was used with `search`",
            token.command
        ));
    }
    if token.query != query || token.requested_mode != requested_mode {
        return Err(anyhow!(
            "cursor does not match the active search query or mode"
        ));
    }
    if token.filters != *filters {
        return Err(anyhow!("cursor does not match the active search filters"));
    }
    if requested_mode != SearchMode::Auto && token.mode_used != requested_mode {
        return Err(anyhow!(
            "cursor mode `{}` does not match the requested search mode `{}`",
            token.mode_used.as_str(),
            requested_mode.as_str()
        ));
    }

    Ok(SearchCursorState::new(
        token.mode_used,
        token.score,
        token.last_seen_at,
        token.snapshot_id,
    ))
}

pub(in crate::cli) fn parse_recent_cursor(
    encoded: &str,
    filters: &RetrievalFilters,
) -> Result<RecentCursorState> {
    let token: RecentCursorToken = decode_cursor(encoded)?;
    if token.command != "recent" {
        return Err(anyhow!(
            "cursor is for command `{}` but was used with `recent`",
            token.command
        ));
    }
    if token.filters != *filters {
        return Err(anyhow!("cursor does not match the active recent filters"));
    }

    Ok(RecentCursorState::new(
        token.last_seen_at,
        token.snapshot_id,
    ))
}

pub(in crate::cli) fn parse_timeline_cursor(
    encoded: &str,
    filters: &RetrievalFilters,
    sort: TimelineSort,
) -> Result<TimelineCursorState> {
    let token: TimelineCursorToken = decode_cursor(encoded)?;
    if token.command != "timeline" {
        return Err(anyhow!(
            "cursor is for command `{}` but was used with `timeline`",
            token.command
        ));
    }
    if token.filters != *filters || token.sort != sort {
        return Err(anyhow!(
            "cursor does not match the active timeline filters or sort"
        ));
    }

    Ok(TimelineCursorState::new(token.observed_at, token.event_id))
}

pub(in crate::cli) fn encode_search_cursor(
    query: &str,
    requested_mode: SearchMode,
    filters: &RetrievalFilters,
    mode_used: SearchMode,
    hit: &crate::model::SearchHit,
) -> Result<String> {
    encode_cursor(&SearchCursorToken {
        command: "search".to_string(),
        query: query.to_string(),
        requested_mode,
        mode_used,
        filters: filters.clone(),
        last_seen_at: hit.last_observed_at().to_string(),
        snapshot_id: hit.snapshot_id(),
        score: hit.score(),
    })
}

pub(in crate::cli) fn encode_recent_cursor(
    filters: &RetrievalFilters,
    hit: &crate::model::SearchHit,
) -> Result<String> {
    encode_cursor(&RecentCursorToken {
        command: "recent".to_string(),
        filters: filters.clone(),
        last_seen_at: hit.last_observed_at().to_string(),
        snapshot_id: hit.snapshot_id(),
    })
}

pub(in crate::cli) fn encode_timeline_cursor(
    filters: &RetrievalFilters,
    sort: TimelineSort,
    event: &TimelineEvent,
) -> Result<String> {
    encode_cursor(&TimelineCursorToken {
        command: "timeline".to_string(),
        filters: filters.clone(),
        sort,
        observed_at: event.observed_at().to_string(),
        event_id: event.event_id(),
    })
}

pub(in crate::cli) fn encode_cursor<T: Serialize>(token: &T) -> Result<String> {
    let json = serde_json::to_vec(token)?;
    Ok(hex::encode(json))
}

pub(in crate::cli) fn decode_cursor<T: for<'de> Deserialize<'de>>(encoded: &str) -> Result<T> {
    let bytes =
        hex::decode(encoded).map_err(|error| anyhow!("invalid cursor encoding: {error}"))?;
    serde_json::from_slice(&bytes).map_err(|error| anyhow!("invalid cursor payload: {error}"))
}