use collections::FxBuildHasher;
use seahash::SeaHasher;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::{
hash::{BuildHasher, Hash, Hasher},
path::PathBuf,
};
use strum::EnumIter;
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct ExternalPaths(pub SmallVec<[PathBuf; 2]>);
impl ExternalPaths {
pub fn paths(&self) -> &[PathBuf] {
&self.0
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ExternalDragPayload {
Files(FileDragPaths),
}
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct FileDragPaths(SmallVec<[(PathBuf, bool); 2]>);
impl FileDragPaths {
pub fn new(entries: impl IntoIterator<Item = (PathBuf, bool)>) -> Self {
Self(entries.into_iter().collect())
}
pub fn entries(&self) -> &[(PathBuf, bool)] {
&self.0
}
}
pub fn hash<T: Hash>(data: &T) -> u64 {
FxBuildHasher.hash_one(data)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClipboardItem {
pub entries: Vec<ClipboardEntry>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ClipboardReadError {
Unavailable,
Denied(String),
UnsupportedContent,
}
impl std::fmt::Display for ClipboardReadError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
Self::Denied(message) => {
write!(formatter, "clipboard access was denied: {message}")
}
Self::UnsupportedContent => {
formatter.write_str("the clipboard contents are unsupported")
}
}
}
}
impl std::error::Error for ClipboardReadError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ClipboardEntry {
String(ClipboardString),
Image(Image),
ExternalPaths(ExternalPaths),
}
impl ClipboardItem {
pub fn new_string(text: String) -> Self {
Self {
entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
}
}
pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
Self {
entries: vec![ClipboardEntry::String(ClipboardString {
text,
metadata: Some(metadata),
})],
}
}
pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
Self {
entries: vec![ClipboardEntry::String(
ClipboardString::new(text).with_json_metadata(metadata),
)],
}
}
pub fn new_image(image: &Image) -> Self {
Self {
entries: vec![ClipboardEntry::Image(image.clone())],
}
}
pub fn text(&self) -> Option<String> {
let mut answer = String::new();
for entry in self.entries.iter() {
if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
answer.push_str(text);
}
}
if answer.is_empty() {
for entry in self.entries.iter() {
if let ClipboardEntry::ExternalPaths(paths) = entry {
for path in &paths.0 {
use std::fmt::Write as _;
_ = write!(answer, "{}", path.display());
}
}
}
}
if !answer.is_empty() {
Some(answer)
} else {
None
}
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub fn metadata(&self) -> Option<&String> {
match self.entries().first() {
Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
clipboard_string.metadata.as_ref()
}
_ => None,
}
}
pub fn entries(&self) -> &[ClipboardEntry] {
&self.entries
}
pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
self.entries.into_iter()
}
}
impl From<ClipboardString> for ClipboardEntry {
fn from(value: ClipboardString) -> Self {
Self::String(value)
}
}
impl From<String> for ClipboardEntry {
fn from(value: String) -> Self {
Self::from(ClipboardString::from(value))
}
}
impl From<Image> for ClipboardEntry {
fn from(value: Image) -> Self {
Self::Image(value)
}
}
impl From<ClipboardEntry> for ClipboardItem {
fn from(value: ClipboardEntry) -> Self {
Self {
entries: vec![value],
}
}
}
impl From<String> for ClipboardItem {
fn from(value: String) -> Self {
Self::from(ClipboardEntry::from(value))
}
}
impl From<Image> for ClipboardItem {
fn from(value: Image) -> Self {
Self::from(ClipboardEntry::from(value))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
pub enum ImageFormat {
Png,
Jpeg,
Webp,
Gif,
Svg,
Bmp,
Tiff,
Ico,
Pnm,
}
impl ImageFormat {
pub const fn mime_type(self) -> &'static str {
match self {
ImageFormat::Png => "image/png",
ImageFormat::Jpeg => "image/jpeg",
ImageFormat::Webp => "image/webp",
ImageFormat::Gif => "image/gif",
ImageFormat::Svg => "image/svg+xml",
ImageFormat::Bmp => "image/bmp",
ImageFormat::Tiff => "image/tiff",
ImageFormat::Ico => "image/ico",
ImageFormat::Pnm => "image/x-portable-anymap",
}
}
pub const fn extension(self) -> &'static str {
match self {
ImageFormat::Png => "png",
ImageFormat::Jpeg => "jpg",
ImageFormat::Webp => "webp",
ImageFormat::Gif => "gif",
ImageFormat::Svg => "svg",
ImageFormat::Bmp => "bmp",
ImageFormat::Tiff => "tiff",
ImageFormat::Ico => "ico",
ImageFormat::Pnm => "pnm",
}
}
pub fn from_mime_type(mime_type: &str) -> Option<Self> {
use strum::IntoEnumIterator;
Self::iter()
.find(|format| format.mime_type() == mime_type)
.or_else(|| Self::from_mime_type_alias(mime_type))
}
fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
match mime_type {
"image/jpg" => Some(Self::Jpeg),
"image/tif" => Some(Self::Tiff),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Image {
pub format: ImageFormat,
pub bytes: Vec<u8>,
pub id: u64,
}
impl Hash for Image {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u64(self.id);
}
}
impl Image {
pub fn empty() -> Self {
Self::from_bytes(ImageFormat::Png, Vec::new())
}
pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
Self {
id: hash(&bytes),
format,
bytes,
}
}
pub fn id(&self) -> u64 {
self.id
}
pub fn format(&self) -> ImageFormat {
self.format
}
pub fn bytes(&self) -> &[u8] {
self.bytes.as_slice()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClipboardString {
pub text: String,
pub metadata: Option<String>,
}
impl ClipboardString {
pub fn new(text: String) -> Self {
Self {
text,
metadata: None,
}
}
pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
self.metadata = Some(serde_json::to_string(&metadata).unwrap());
self
}
pub fn text(&self) -> &String {
&self.text
}
pub fn into_text(self) -> String {
self.text
}
pub fn metadata_json<T>(&self) -> Option<T>
where
T: for<'a> Deserialize<'a>,
{
self.metadata
.as_ref()
.and_then(|m| serde_json::from_str(m).ok())
}
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
pub fn text_hash(text: &str) -> u64 {
let mut hasher = SeaHasher::new();
text.hash(&mut hasher);
hasher.finish()
}
}
impl From<String> for ClipboardString {
fn from(value: String) -> Self {
Self {
text: value,
metadata: None,
}
}
}