use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContentType {
Text,
Image,
Files,
}
impl ContentType {
pub fn as_str(&self) -> &'static str {
match self {
ContentType::Text => "text",
ContentType::Image => "image",
ContentType::Files => "files",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"text" => Some(ContentType::Text),
"image" => Some(ContentType::Image),
"files" => Some(ContentType::Files),
_ => None,
}
}
}
impl fmt::Display for ContentType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub enum ClipboardContent {
Text(String),
Image {
data: Vec<u8>,
width: u32,
height: u32,
},
Files(Vec<String>),
}
impl ClipboardContent {
pub fn content_type(&self) -> ContentType {
match self {
ClipboardContent::Text(_) => ContentType::Text,
ClipboardContent::Image { .. } => ContentType::Image,
ClipboardContent::Files(_) => ContentType::Files,
}
}
pub fn preview(&self) -> String {
match self {
ClipboardContent::Text(text) => {
let preview: String = text.chars().take(200).collect();
if text.len() > 200 { format!("{}...", preview) } else { preview }
}
ClipboardContent::Image { width, height, .. } => {
format!("Image {}x{}", width, height)
}
ClipboardContent::Files(files) => {
if files.len() == 1 {
files[0].clone()
} else {
format!("{} files", files.len())
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SensitiveDataType {
Password,
ApiKey,
PrivateKey,
CreditCard,
}
impl SensitiveDataType {
pub fn as_str(&self) -> &'static str {
match self {
SensitiveDataType::Password => "password",
SensitiveDataType::ApiKey => "api_key",
SensitiveDataType::PrivateKey => "private_key",
SensitiveDataType::CreditCard => "credit_card",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"password" => Some(SensitiveDataType::Password),
"api_key" => Some(SensitiveDataType::ApiKey),
"private_key" => Some(SensitiveDataType::PrivateKey),
"credit_card" => Some(SensitiveDataType::CreditCard),
_ => None,
}
}
}
impl fmt::Display for SensitiveDataType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tag {
pub id: String,
pub name: String,
pub color: String,
}
impl Tag {
pub fn new(id: impl Into<String>, name: impl Into<String>, color: impl Into<String>) -> Self {
Self { id: id.into(), name: name.into(), color: color.into() }
}
}
#[derive(Debug, Clone)]
pub struct ClipboardEntry {
pub id: String,
pub content_type: ContentType,
pub content: ClipboardContent,
pub preview_text: String,
pub hash: String,
pub is_sensitive: bool,
pub sensitive_type: Option<SensitiveDataType>,
pub is_favorite: bool,
pub is_pinned: bool,
pub created_at: String,
pub accessed_at: String,
pub access_count: u32,
pub source_app: Option<String>,
pub tags: Vec<Tag>,
}
impl ClipboardEntry {
pub fn new(
id: impl Into<String>,
content: ClipboardContent,
hash: impl Into<String>,
created_at: impl Into<String>,
) -> Self {
let preview_text = content.preview();
let content_type = content.content_type();
let created = created_at.into();
Self {
id: id.into(),
content_type,
content,
preview_text,
hash: hash.into(),
is_sensitive: false,
sensitive_type: None,
is_favorite: false,
is_pinned: false,
created_at: created.clone(),
accessed_at: created,
access_count: 1,
source_app: None,
tags: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CategoryFilter {
#[default]
All,
Text,
Images,
Files,
Favorites,
Sensitive,
}
impl CategoryFilter {
pub fn display_name(&self) -> &'static str {
match self {
CategoryFilter::All => "All",
CategoryFilter::Text => "Text",
CategoryFilter::Images => "Images",
CategoryFilter::Files => "Files",
CategoryFilter::Favorites => "Favorites",
CategoryFilter::Sensitive => "Sensitive",
}
}
pub fn all_options() -> &'static [CategoryFilter] {
&[
CategoryFilter::All,
CategoryFilter::Text,
CategoryFilter::Images,
CategoryFilter::Files,
CategoryFilter::Favorites,
CategoryFilter::Sensitive,
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_type_conversion() {
assert_eq!(ContentType::Text.as_str(), "text");
assert_eq!(ContentType::parse("text"), Some(ContentType::Text));
assert_eq!(ContentType::parse("unknown"), None);
}
#[test]
fn test_clipboard_content_preview() {
let text = ClipboardContent::Text("Hello, World!".to_string());
assert_eq!(text.preview(), "Hello, World!");
let long_text = ClipboardContent::Text("a".repeat(250));
assert!(long_text.preview().ends_with("..."));
assert!(long_text.preview().len() < 250);
let image = ClipboardContent::Image { data: vec![], width: 1920, height: 1080 };
assert_eq!(image.preview(), "Image 1920x1080");
let files = ClipboardContent::Files(vec![
"/path/to/file1.txt".to_string(),
"/path/to/file2.txt".to_string(),
]);
assert_eq!(files.preview(), "2 files");
}
#[test]
fn test_sensitive_data_type() {
assert_eq!(SensitiveDataType::Password.as_str(), "password");
assert_eq!(SensitiveDataType::parse("api_key"), Some(SensitiveDataType::ApiKey));
}
}