use anyhow::Result;
use async_compression::tokio::bufread::{BrotliDecoder, DeflateDecoder, GzipDecoder, ZstdDecoder};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use serde::Serializer;
use std::sync::{Arc, LazyLock};
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use tokio::{
fs::{self, File, OpenOptions},
io::{AsyncRead, AsyncReadExt, BufReader, BufWriter},
};
use unicode_width::UnicodeWidthStr;
pub const ENCODING_EXTS: [(&str, &str); 4] = [
("deflate", ".enc.deflate"),
("gzip", ".enc.gz"),
("br", ".enc.br"),
("zstd", ".enc.zst"),
];
static CLIPBOARD: LazyLock<Arc<std::sync::Mutex<Option<arboard::Clipboard>>>> =
LazyLock::new(|| std::sync::Arc::new(std::sync::Mutex::new(arboard::Clipboard::new().ok())));
pub fn base64_encode(data: &[u8]) -> String {
STANDARD.encode(data)
}
pub fn ellipsis_tail(text: &str, width: u16) -> String {
let width = width as _;
let text_width = text.width();
if text_width > width {
format!("{}…", &text[..width - 1])
} else {
text.to_string()
}
}
pub fn ellipsis_head(text: &str, width: u16) -> String {
let width = width as _;
let text_width = text.width();
if text_width > width {
format!("…{}", &text[text_width - width + 1..])
} else {
text.to_string()
}
}
pub fn format_size(bytes: Option<u64>) -> String {
match bytes {
None => String::new(),
Some(0) => "0".to_string(),
Some(bytes) => {
let prefix = ["b", "kb", "mb", "gb", "tb"];
let mut i = 0;
while i < prefix.len() && 1024u64.pow(i as u32 + 1) <= bytes {
i += 1;
}
let precision = if bytes % 1024u64.pow(i as u32) == 0 {
0
} else {
1
};
format!(
"{:.prec$}{}",
bytes as f64 / 1024f64.powi(i as i32),
prefix[i],
prec = precision
)
}
}
}
pub fn format_time_delta(delta: Option<u64>) -> String {
let mut delta = match delta {
Some(ms) => ms,
None => return String::from(""),
};
if delta == 0 {
return String::from("0");
}
if delta > 1000 && delta < 10000 {
let seconds = delta as f64 / 1000.0;
return format!("{:.2}s", seconds);
}
let prefix = ["ms", "s", "min", "h"];
let div = [1000, 60, 60];
let mut i = 0;
while i < div.len() && delta >= div[i] {
delta /= div[i];
i += 1;
}
format!("{}{}", delta, prefix[i])
}
pub fn next_idx(len: usize, idx: usize) -> usize {
if idx >= len.saturating_sub(1) {
0
} else {
idx + 1
}
}
pub fn prev_idx(len: usize, idx: usize) -> usize {
if idx == 0 {
len.saturating_sub(1)
} else {
idx - 1
}
}
#[cfg(not(any(target_os = "android", target_os = "emscripten")))]
pub fn set_text(text: &str) -> anyhow::Result<()> {
let mut clipboard = CLIPBOARD.lock().unwrap();
match clipboard.as_mut() {
Some(clipboard) => clipboard.set_text(text)?,
None => anyhow::bail!("No available clipboard"),
}
Ok(())
}
#[cfg(any(target_os = "android", target_os = "emscripten"))]
pub fn set_text(_text: &str) -> anyhow::Result<()> {
anyhow::bail!("No available clipboard")
}
pub async fn uncompress_data(encoding: &str, path: &str) -> Result<Vec<u8>> {
let file = File::open(path).await?;
let reader = BufReader::new(file);
let mut decompressed = Vec::new();
let mut decoder = uncompress_decoder(encoding, reader);
decoder.read_to_end(&mut decompressed).await?;
Ok(decompressed)
}
pub async fn uncompress_file(encoding: &str, source_path: &str, target_path: &str) -> Result<()> {
let source_file = File::open(source_path).await?;
let target_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(target_path)
.await?;
let reader = BufReader::new(source_file);
let mut decoder = uncompress_decoder(encoding, reader);
let mut writer = BufWriter::new(target_file);
tokio::io::copy(&mut decoder, &mut writer).await?;
fs::remove_file(source_path).await?;
Ok(())
}
fn uncompress_decoder(
encoding: &str,
reader: BufReader<File>,
) -> Box<dyn AsyncRead + Send + Unpin> {
match encoding {
"deflate" => Box::new(DeflateDecoder::new(reader)),
"gzip" => Box::new(GzipDecoder::new(reader)),
"br" => Box::new(BrotliDecoder::new(reader)),
"zstd" => Box::new(ZstdDecoder::new(reader)),
_ => Box::new(reader),
}
}
pub fn to_ext_name(mime: &str) -> &str {
match mime {
"audio/aac" => ".aac",
"application/x-abiword" => ".abw",
"image/apng" => ".apng",
"application/x-freearc" => ".arc",
"image/avif" => ".avif",
"video/x-msvideo" => ".avi",
"application/vnd.amazon.ebook" => ".azw",
"application/octet-stream" => ".bin",
"image/bmp" => ".bmp",
"application/x-bzip" => ".bz",
"application/x-bzip2" => ".bz2",
"application/x-cdf" => ".cda",
"application/x-csh" => ".csh",
"text/css" => ".css",
"text/csv" => ".csv",
"application/msword" => ".doc",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" => ".docx",
"application/vnd.ms-fontobject" => ".eot",
"application/epub+zip" => ".epub",
"application/gzip" | "application/x-gzip" => ".gz",
"image/gif" => ".gif",
"text/html" | "text/htm" => ".html",
"image/vnd.microsoft.icon" => ".ico",
"text/calendar" => ".ics",
"application/java-archive" => ".jar",
"image/jpeg" => ".jpeg",
"text/javascript" => ".js",
"application/json" => ".json",
"application/ld+json" => ".jsonld",
"audio/midi" | "audio/x-midi" => ".mid",
"audio/mpeg" => ".mp3",
"video/mp4" => ".mp4",
"video/mpeg" => ".mpeg",
"application/vnd.apple.installer+xml" => ".mpkg",
"application/vnd.oasis.opendocument.presentation" => ".odp",
"application/vnd.oasis.opendocument.spreadsheet" => ".ods",
"application/vnd.oasis.opendocument.text" => ".odt",
"audio/ogg" => ".oga",
"video/ogg" => ".ogv",
"application/ogg" => ".ogx",
"font/otf" => ".otf",
"image/png" => ".png",
"application/pdf" => ".pdf",
"application/x-httpd-php" => ".php",
"application/vnd.ms-powerpoint" => ".ppt",
"application/vnd.openxmlformats-officedocument.presentationml.presentation" => ".pptx",
"application/vnd.rar" => ".rar",
"application/rtf" => ".rtf",
"application/x-sh" => ".sh",
"image/svg+xml" => ".svg",
"application/x-tar" => ".tar",
"image/tiff" => ".tif",
"video/mp2t" => ".ts",
"font/ttf" => ".ttf",
"text/plain" => ".txt",
"application/vnd.visio" => ".vsd",
"audio/wav" => ".wav",
"audio/webm" => ".weba",
"video/webm" => ".webm",
"image/webp" => ".webp",
"font/woff" => ".woff",
"font/woff2" => ".woff2",
"application/xhtml+xml" => ".xhtml",
"application/vnd.ms-excel" => ".xls",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" => ".xlsx",
"application/xml" | "text/xml" => ".xml",
"application/vnd.mozilla.xul+xml" => ".xul",
"application/zip" | "x-zip-compressed" => ".zip",
"video/3gpp" | "audio/3gpp" => ".3gp",
"video/3gpp2" | "audio/3gpp2" => ".3g2",
"application/x-7z-compressed" => ".7z",
_ => {
if mime.starts_with("text/") {
".txt"
} else {
""
}
}
}
}
pub fn to_md_lang(mime: &str) -> &str {
if let Some(value) = mime
.strip_prefix("text/")
.or_else(|| mime.strip_prefix("application/"))
{
if let Some(value) = value.strip_prefix("x-") {
value
} else {
value
}
} else {
""
}
}
pub fn serialize_datetime<S>(date: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let formatted = date.format(&Rfc3339).map_err(serde::ser::Error::custom)?;
serializer.serialize_str(&formatted)
}
pub fn serialize_option_datetime<S>(
date: &Option<OffsetDateTime>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match date {
Some(date) => serialize_datetime(date, serializer),
None => serializer.serialize_none(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_md_lang() {
assert_eq!(to_md_lang("application/json"), "json");
assert_eq!(to_md_lang("application/xml"), "xml");
assert_eq!(to_md_lang("application/octet-stream"), "octet-stream");
assert_eq!(to_md_lang("application/javascript"), "javascript");
assert_eq!(to_md_lang("text/x-rust"), "rust");
assert_eq!(to_md_lang("text/css"), "css");
}
}