clipmem 0.5.5

macOS clipboard memory backed by SQLite and searchable from agent runtimes
Documentation
use std::error::Error;
use std::fmt;
use std::str::FromStr;

use serde::Serialize;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ClipboardKind {
    PlainText,
    Url,
    FileUrl,
    Html,
    Json,
    Xml,
    Rtf,
    Pdf,
    Image,
    Binary,
    Empty,
}

impl ClipboardKind {
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::PlainText => "plain_text",
            Self::Url => "url",
            Self::FileUrl => "file_url",
            Self::Html => "html",
            Self::Json => "json",
            Self::Xml => "xml",
            Self::Rtf => "rtf",
            Self::Pdf => "pdf",
            Self::Image => "image",
            Self::Binary => "binary",
            Self::Empty => "empty",
        }
    }

    #[must_use]
    pub const fn is_textual(self) -> bool {
        matches!(
            self,
            Self::PlainText
                | Self::Url
                | Self::FileUrl
                | Self::Html
                | Self::Rtf
                | Self::Json
                | Self::Xml
        )
    }

    #[must_use]
    pub const fn priority(self) -> u8 {
        match self {
            Self::PlainText => 0,
            Self::Url => 1,
            Self::FileUrl => 2,
            Self::Html => 3,
            Self::Json => 4,
            Self::Xml => 5,
            Self::Rtf => 6,
            Self::Pdf => 7,
            Self::Image => 8,
            Self::Binary => 9,
            Self::Empty => 10,
        }
    }
}

impl fmt::Display for ClipboardKind {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for ClipboardKind {
    type Err = ParseDomainValueError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value {
            "plain_text" => Ok(Self::PlainText),
            "url" => Ok(Self::Url),
            "file_url" => Ok(Self::FileUrl),
            "html" => Ok(Self::Html),
            "json" => Ok(Self::Json),
            "xml" => Ok(Self::Xml),
            "rtf" => Ok(Self::Rtf),
            "pdf" => Ok(Self::Pdf),
            "image" => Ok(Self::Image),
            "binary" => Ok(Self::Binary),
            "empty" => Ok(Self::Empty),
            _ => Err(ParseDomainValueError::new("clipboard kind", value)),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SnapshotKind {
    Empty,
    Mixed,
    PlainText,
    Url,
    FileUrl,
    Html,
    Json,
    Xml,
    Rtf,
    Pdf,
    Image,
    Binary,
}

impl SnapshotKind {
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Empty => "empty",
            Self::Mixed => "mixed",
            Self::PlainText => "plain_text",
            Self::Url => "url",
            Self::FileUrl => "file_url",
            Self::Html => "html",
            Self::Json => "json",
            Self::Xml => "xml",
            Self::Rtf => "rtf",
            Self::Pdf => "pdf",
            Self::Image => "image",
            Self::Binary => "binary",
        }
    }
}

impl fmt::Display for SnapshotKind {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl From<ClipboardKind> for SnapshotKind {
    fn from(value: ClipboardKind) -> Self {
        match value {
            ClipboardKind::PlainText => Self::PlainText,
            ClipboardKind::Url => Self::Url,
            ClipboardKind::FileUrl => Self::FileUrl,
            ClipboardKind::Html => Self::Html,
            ClipboardKind::Json => Self::Json,
            ClipboardKind::Xml => Self::Xml,
            ClipboardKind::Rtf => Self::Rtf,
            ClipboardKind::Pdf => Self::Pdf,
            ClipboardKind::Image => Self::Image,
            ClipboardKind::Binary => Self::Binary,
            ClipboardKind::Empty => Self::Empty,
        }
    }
}

impl FromStr for SnapshotKind {
    type Err = ParseDomainValueError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value {
            "empty" => Ok(Self::Empty),
            "mixed" => Ok(Self::Mixed),
            "plain_text" => Ok(Self::PlainText),
            "url" => Ok(Self::Url),
            "file_url" => Ok(Self::FileUrl),
            "html" => Ok(Self::Html),
            "json" => Ok(Self::Json),
            "xml" => Ok(Self::Xml),
            "rtf" => Ok(Self::Rtf),
            "pdf" => Ok(Self::Pdf),
            "image" => Ok(Self::Image),
            "binary" => Ok(Self::Binary),
            _ => Err(ParseDomainValueError::new("snapshot kind", value)),
        }
    }
}

#[derive(Debug, Clone)]
pub struct ParseDomainValueError {
    domain: &'static str,
    value: String,
}

impl ParseDomainValueError {
    pub(crate) fn new(domain: &'static str, value: impl Into<String>) -> Self {
        Self {
            domain,
            value: value.into(),
        }
    }
}

impl fmt::Display for ParseDomainValueError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "unknown {} value '{}'", self.domain, self.value)
    }
}

impl Error for ParseDomainValueError {}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use super::{ClipboardKind, SnapshotKind};

    #[test]
    fn clipboard_kind_round_trips_through_strings() {
        assert_eq!(
            ClipboardKind::from_str(ClipboardKind::Html.as_str()).expect("kind should parse"),
            ClipboardKind::Html
        );
        assert!(ClipboardKind::Html.is_textual());
        assert!(ClipboardKind::Image.priority() > ClipboardKind::Url.priority());
    }

    #[test]
    fn snapshot_kind_parses_and_maps_from_clipboard_kind() {
        assert_eq!(
            SnapshotKind::from(ClipboardKind::PlainText),
            SnapshotKind::PlainText
        );
        assert_eq!(
            SnapshotKind::from_str("mixed").expect("snapshot kind should parse"),
            SnapshotKind::Mixed
        );
    }
}