use clap::ValueEnum;
use eyre::{ContextCompat, Error, bail};
use image::{
DynamicImage,
codecs::png::{CompressionType, FilterType, PngEncoder},
};
#[cfg(feature = "jxl")]
use jxl_encoder::{LosslessConfig, LossyConfig, PixelLayout};
use serde::{Deserialize, Serialize};
use std::{
env,
fmt::Display,
io::Cursor,
path::{Path, PathBuf},
result::Result,
str::FromStr,
};
use chrono::Local;
#[cfg(any(feature = "selector", feature = "color_picker"))]
use libwayshot::{
Result as WayshotResult,
region::{LogicalRegion, Position, Region, Size},
};
use crate::config::{Jxl, Png};
#[cfg(feature = "completions")]
pub fn print_completions(shell: crate::cli::Shell) {
use clap::CommandFactory;
use clap_complete::generate;
let mut cmd = crate::cli::Cli::command();
let mut out = std::io::stdout().lock();
match shell {
crate::cli::Shell::Bash => {
generate(clap_complete::shells::Bash, &mut cmd, "wayshot", &mut out)
}
crate::cli::Shell::Elvish => {
generate(clap_complete::shells::Elvish, &mut cmd, "wayshot", &mut out)
}
crate::cli::Shell::Fish => {
generate(clap_complete::shells::Fish, &mut cmd, "wayshot", &mut out)
}
crate::cli::Shell::Pwsh => generate(
clap_complete::shells::PowerShell,
&mut cmd,
"wayshot",
&mut out,
),
crate::cli::Shell::Zsh => {
generate(clap_complete::shells::Zsh, &mut cmd, "wayshot", &mut out)
}
crate::cli::Shell::Nushell => generate(
clap_complete_nushell::Nushell,
&mut cmd,
"wayshot",
&mut out,
),
}
}
pub fn parse_slurp_geometry(s: &str) -> Result<libwayshot::LogicalRegion, String> {
let s = s.trim();
if s.is_empty() {
return Err("geometry string is empty".to_string());
}
let (pos, size) = s
.split_once(|c: char| c.is_ascii_whitespace())
.ok_or_else(|| format!("invalid geometry: expected 'x,y widthxheight', got '{s}'"))?;
let (x, y) = pos
.split_once(',')
.ok_or_else(|| format!("invalid position: expected 'x,y', got '{pos}'"))?;
let (w, h) = size
.split_once('x')
.ok_or_else(|| format!("invalid size: expected 'widthxheight', got '{size}'"))?;
let x: i32 = x
.trim()
.parse()
.map_err(|_| format!("invalid x coordinate: {x}"))?;
let y: i32 = y
.trim()
.parse()
.map_err(|_| format!("invalid y coordinate: {y}"))?;
let w: u32 = w
.trim()
.parse()
.map_err(|_| format!("invalid width: {w}"))?;
let h: u32 = h
.trim()
.parse()
.map_err(|_| format!("invalid height: {h}"))?;
if w == 0 || h == 0 {
return Err("width and height must be positive".to_string());
}
Ok(libwayshot::LogicalRegion {
inner: libwayshot::region::Region {
position: libwayshot::region::Position { x, y },
size: libwayshot::region::Size {
width: w,
height: h,
},
},
})
}
#[cfg(any(feature = "selector", feature = "color_picker"))]
pub fn waysip_to_region(
size: libwaysip::Size,
position: libwaysip::Position,
) -> WayshotResult<LogicalRegion> {
let size = Size {
width: size.width.try_into().map_err(|_| {
libwayshot::Error::FreezeCallbackError("width cannot be negative".to_string())
})?,
height: size.height.try_into().map_err(|_| {
libwayshot::Error::FreezeCallbackError("height cannot be negative".to_string())
})?,
};
Ok(LogicalRegion {
inner: Region {
position: Position {
x: position.x,
y: position.y,
},
size,
},
})
}
#[cfg(feature = "selector")]
fn color_from_hex(rgba_hex: String) -> Result<libwaysip::Color, String> {
libwaysip::Color::hex_to_color(rgba_hex.clone())
.map_err(|e| format!("Failed to parse color \"{rgba_hex}\" as rgba hex: {e}"))
}
#[cfg(feature = "selector")]
pub fn get_region_area(
conn: &libwayshot::WayshotConnection,
foreground_color: Option<String>,
background_color: Option<String>,
) -> Result<LogicalRegion, String> {
let mut info = libwaysip::WaySip::new();
if let Some(color) = foreground_color {
info = info.with_border_text_color(color_from_hex(color)?);
}
if let Some(color) = background_color {
info = info.with_background_color(color_from_hex(color)?);
}
let info = info
.with_connection(conn.conn.clone())
.with_selection_type(libwaysip::SelectionType::Area)
.get()
.map_err(|e| e.to_string())?
.ok_or_else(|| "No area selected".to_string())?;
waysip_to_region(info.size(), info.left_top_point()).map_err(|e| e.to_string())
}
#[cfg(feature = "color_picker")]
pub fn get_region_point(conn: &libwayshot::WayshotConnection) -> Result<LogicalRegion, String> {
let info = libwaysip::WaySip::new()
.with_connection(conn.conn.clone())
.with_selection_type(libwaysip::SelectionType::Point)
.get()
.map_err(|e| e.to_string())?
.ok_or_else(|| "Failed to capture the point".to_string())?;
waysip_to_region(
libwaysip::Size {
width: 1,
height: 1,
},
info.left_top_point(),
)
.map_err(|e| e.to_string())
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum EncodingFormat {
#[cfg(feature = "jpeg")]
Jpg,
#[default]
Png,
#[cfg(feature = "pnm")]
Ppm,
#[cfg(feature = "qoi")]
Qoi,
#[cfg(feature = "webp")]
Webp,
#[cfg(feature = "avif")]
Avif,
#[cfg(feature = "jxl")]
Jxl,
}
impl From<EncodingFormat> for image::ImageFormat {
fn from(format: EncodingFormat) -> Self {
match format {
#[cfg(feature = "jpeg")]
EncodingFormat::Jpg => image::ImageFormat::Jpeg,
EncodingFormat::Png => image::ImageFormat::Png,
#[cfg(feature = "pnm")]
EncodingFormat::Ppm => image::ImageFormat::Pnm,
#[cfg(feature = "qoi")]
EncodingFormat::Qoi => image::ImageFormat::Qoi,
#[cfg(feature = "webp")]
EncodingFormat::Webp => image::ImageFormat::WebP,
#[cfg(feature = "avif")]
EncodingFormat::Avif => image::ImageFormat::Avif,
#[cfg(feature = "jxl")]
EncodingFormat::Jxl => image::ImageFormat::Png,
}
}
}
impl TryFrom<&PathBuf> for EncodingFormat {
type Error = Error;
fn try_from(value: &PathBuf) -> Result<Self, Self::Error> {
value
.extension()
.wrap_err_with(|| {
format!(
"no extension in {} to deduce encoding format",
value.display()
)
})
.and_then(|ext| {
ext.to_str().wrap_err_with(|| {
format!("extension in {} is not valid unicode", value.display())
})
})
.and_then(|ext| ext.parse())
}
}
impl Display for EncodingFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", Into::<&str>::into(*self))
}
}
impl From<EncodingFormat> for &str {
fn from(format: EncodingFormat) -> Self {
match format {
#[cfg(feature = "jpeg")]
EncodingFormat::Jpg => "jpg",
EncodingFormat::Png => "png",
#[cfg(feature = "pnm")]
EncodingFormat::Ppm => "ppm",
#[cfg(feature = "qoi")]
EncodingFormat::Qoi => "qoi",
#[cfg(feature = "webp")]
EncodingFormat::Webp => "webp",
#[cfg(feature = "avif")]
EncodingFormat::Avif => "avif",
#[cfg(feature = "jxl")]
EncodingFormat::Jxl => "jxl",
}
}
}
impl FromStr for EncodingFormat {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
#[cfg(feature = "jpeg")]
"jpg" | "jpeg" => Self::Jpg,
"png" => Self::Png,
#[cfg(feature = "pnm")]
"ppm" => Self::Ppm,
#[cfg(feature = "qoi")]
"qoi" => Self::Qoi,
#[cfg(feature = "webp")]
"webp" => Self::Webp,
#[cfg(feature = "avif")]
"avif" => Self::Avif,
#[cfg(feature = "jxl")]
"jxl" => Self::Jxl,
_ => bail!("unsupported extension '{s}'"),
})
}
}
pub fn get_absolute_path(path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
env::current_dir().unwrap_or_default().join(path)
}
}
pub fn get_expanded_path(path: &Path) -> PathBuf {
let path_str = path.to_string_lossy();
match shellexpand::full(&path_str) {
Ok(expanded) => PathBuf::from(expanded.into_owned()),
Err(_) => env::current_dir().unwrap_or_default(),
}
}
pub fn get_default_file_name(filename_format: &str, encoding: EncodingFormat) -> PathBuf {
PathBuf::from(format!(
"{}.{encoding}",
Local::now().format(filename_format)
))
}
pub fn get_full_file_name(path: &Path, filename_format: &str, encoding: EncodingFormat) -> PathBuf {
let absolute = get_absolute_path(&get_expanded_path(path));
if absolute.is_dir() {
absolute.join(get_default_file_name(filename_format, encoding))
} else {
let base_dir = absolute
.parent()
.map(PathBuf::from)
.unwrap_or_else(|| env::current_dir().unwrap_or_default());
let stem = absolute.file_stem().unwrap_or_default().to_string_lossy();
base_dir.join(format!("{stem}.{encoding}"))
}
}
pub fn encode_image(
image: &DynamicImage,
encoding: EncodingFormat,
jxl: &Jxl,
png: &Png,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
#[cfg(not(feature = "jxl"))]
let _ = jxl;
match encoding {
#[cfg(feature = "jxl")]
EncodingFormat::Jxl => encode_to_jxl_bytes(
image,
jxl.get_lossless(),
jxl.get_distance(),
jxl.get_effort(),
),
EncodingFormat::Png => encode_to_png_bytes(image, png.get_compression(), png.get_filter()),
#[cfg(any(
feature = "jpeg",
feature = "pnm",
feature = "qoi",
feature = "webp",
feature = "avif"
))]
_ => {
let mut buf = Cursor::new(Vec::new());
image.write_to(&mut buf, encoding.into())?;
Ok(buf.into_inner())
}
}
}
#[cfg(feature = "jxl")]
fn encode_to_jxl_bytes(
image: &DynamicImage,
lossless: bool,
distance: f32,
effort: u8,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let pixels_rgb8 = image.to_rgb8();
let pixels = pixels_rgb8.as_raw();
let w = image.width();
let h = image.height();
if lossless {
Ok(LosslessConfig::new()
.with_effort(effort)
.encode(pixels, w, h, PixelLayout::Rgb8)?)
} else {
Ok(LossyConfig::new(distance).with_effort(effort).encode(
pixels,
w,
h,
PixelLayout::Rgb8,
)?)
}
}
fn encode_to_png_bytes(
image: &DynamicImage,
compression: CompressionType,
filter: FilterType,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut buf = Cursor::new(Vec::new());
image.write_with_encoder(PngEncoder::new_with_quality(&mut buf, compression, filter))?;
Ok(buf.into_inner())
}