Make Cosmic Files show .exe icons (#2070)
* [Feature Request] Make Cosmic Files show .exe icons like Dolphin * Sped Up Icon Extraction * Compacted Code * Replace wrestool with pelite crate for .exe icon extraction * Run PE icon extraction as external thumbnailer * Use dedicated external EXE thumbnailer * Remove embedded EXE thumbnailer * Move cosmic-files-thumbnailer to new crate --------- Co-authored-by: ZlordHUN <137647604+ZlordHUN@users.noreply.github.com>
This commit is contained in:
parent
c0b8a8c60a
commit
b326e48cf6
8 changed files with 356 additions and 9 deletions
48
Cargo.lock
generated
48
Cargo.lock
generated
|
|
@ -1470,6 +1470,14 @@ dependencies = [
|
||||||
"zbus 4.4.0",
|
"zbus 4.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cosmic-files-thumbnailer"
|
||||||
|
version = "1.9.0"
|
||||||
|
dependencies = [
|
||||||
|
"image",
|
||||||
|
"pelite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-freedesktop-icons"
|
name = "cosmic-freedesktop-icons"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
@ -1793,6 +1801,15 @@ version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dataview"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daba87f72c730b508641c9fb6411fc9bba73939eed2cab611c399500511880d0"
|
||||||
|
dependencies = [
|
||||||
|
"derive_pod",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deflate64"
|
name = "deflate64"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
|
|
@ -1839,6 +1856,12 @@ dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_pod"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_setters"
|
name = "derive_setters"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
|
|
@ -4980,6 +5003,12 @@ dependencies = [
|
||||||
"memoffset",
|
"memoffset",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "no-std-compat"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "no_std_io2"
|
name = "no_std_io2"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|
@ -5578,6 +5607,25 @@ dependencies = [
|
||||||
"hmac",
|
"hmac",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pelite"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685"
|
||||||
|
dependencies = [
|
||||||
|
"dataview",
|
||||||
|
"libc",
|
||||||
|
"no-std-compat",
|
||||||
|
"pelite-macros",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pelite-macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
|
|
|
||||||
10
Cargo.toml
10
Cargo.toml
|
|
@ -18,9 +18,9 @@ gio = { version = "0.21", optional = true }
|
||||||
glib = { version = "0.21", optional = true }
|
glib = { version = "0.21", optional = true }
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
ignore = "0.4"
|
ignore = "0.4"
|
||||||
image = "0.25"
|
image.workspace = true
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4"
|
log.workspace = true
|
||||||
mime_guess = "2"
|
mime_guess = "2"
|
||||||
notify-debouncer-full = "0.7"
|
notify-debouncer-full = "0.7"
|
||||||
notify-rust = { version = "4", optional = true }
|
notify-rust = { version = "4", optional = true }
|
||||||
|
|
@ -162,4 +162,8 @@ tokio = { version = "1", features = ["rt", "macros"] }
|
||||||
# smithay-clipboard = { path = "../smithay-clipboard" }
|
# smithay-clipboard = { path = "../smithay-clipboard" }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["cosmic-files-applet"]
|
members = ["cosmic-files-applet", "cosmic-files-thumbnailer"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
image = "0.25"
|
||||||
|
log = "0.4"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ version = "1.9.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4"
|
log.workspace = true
|
||||||
zbus = "4" # Blocking connection in zbus 5 hangs
|
zbus = "4" # Blocking connection in zbus 5 hangs
|
||||||
|
|
||||||
[dependencies.cosmic-files]
|
[dependencies.cosmic-files]
|
||||||
|
|
|
||||||
8
cosmic-files-thumbnailer/Cargo.toml
Normal file
8
cosmic-files-thumbnailer/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "cosmic-files-thumbnailer"
|
||||||
|
version = "1.9.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
image.workspace = true
|
||||||
|
pelite = "0.10"
|
||||||
254
cosmic-files-thumbnailer/src/main.rs
Normal file
254
cosmic-files-thumbnailer/src/main.rs
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
use image::{DynamicImage, ImageFormat, RgbaImage};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
thumbnail_from_args(std::env::args_os().skip(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn thumbnail_from_args(mut args: impl Iterator<Item = OsString>) -> Result<(), Box<dyn Error>> {
|
||||||
|
let usage = || {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"usage: cosmic-files-thumbnailer OUTPUT --size SIZE INPUT",
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let output = args.next().ok_or_else(usage)?;
|
||||||
|
if args.next().as_deref() != Some(OsStr::new("--size")) {
|
||||||
|
return Err(usage().into());
|
||||||
|
}
|
||||||
|
let size = args
|
||||||
|
.next()
|
||||||
|
.and_then(|arg| arg.into_string().ok())
|
||||||
|
.and_then(|arg| arg.parse::<u32>().ok())
|
||||||
|
.filter(|size| *size > 0)
|
||||||
|
.ok_or_else(usage)?;
|
||||||
|
let input = args.next().ok_or_else(usage)?;
|
||||||
|
if args.next().is_some() {
|
||||||
|
return Err(usage().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbnail(Path::new(&input), Path::new(&output), size)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn thumbnail(input: &Path, output: &Path, size: u32) -> Result<(), Box<dyn Error>> {
|
||||||
|
let icon = thumbnail_image(input, size)?;
|
||||||
|
DynamicImage::ImageRgba8(icon).save_with_format(output, ImageFormat::Png)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thumbnail_image(input: &Path, size: u32) -> Result<RgbaImage, Box<dyn Error>> {
|
||||||
|
if size == 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"thumbnail size must be positive",
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the PE so only the headers and resource pages touched by pelite are read.
|
||||||
|
let data = pelite::FileMap::open(input)?;
|
||||||
|
let bytes = data.as_ref();
|
||||||
|
if bytes.len() < 64 || !bytes.starts_with(b"MZ") {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "input is not a PE file").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = pelite::PeFile::from_bytes(bytes).map_err(invalid_data)?;
|
||||||
|
let resources = file.resources().map_err(invalid_data)?;
|
||||||
|
let (_name, group) = resources
|
||||||
|
.icons()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "PE file has no icon group"))?
|
||||||
|
.map_err(invalid_data)?;
|
||||||
|
|
||||||
|
let mut ico = Vec::new();
|
||||||
|
group.write(&mut ico).map_err(invalid_data)?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
DynamicImage::ImageRgba8(largest_icon(&ico).map_err(invalid_data)?)
|
||||||
|
.thumbnail(size, size)
|
||||||
|
.into_rgba8(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid_data(error: impl std::fmt::Display) -> io::Error {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidData, error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn largest_icon(ico: &[u8]) -> Result<RgbaImage, String> {
|
||||||
|
if ico.len() < 6 {
|
||||||
|
return Err("ICO header is truncated".to_string());
|
||||||
|
}
|
||||||
|
let count = usize::from(u16::from_le_bytes([ico[4], ico[5]]));
|
||||||
|
let mut best: Option<(u64, RgbaImage)> = None;
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..count.min(32) {
|
||||||
|
let entry = 6 + i * 16;
|
||||||
|
if ico.len() < entry + 16 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let size = u32::from_le_bytes([
|
||||||
|
ico[entry + 8],
|
||||||
|
ico[entry + 9],
|
||||||
|
ico[entry + 10],
|
||||||
|
ico[entry + 11],
|
||||||
|
]) as usize;
|
||||||
|
let offset = u32::from_le_bytes([
|
||||||
|
ico[entry + 12],
|
||||||
|
ico[entry + 13],
|
||||||
|
ico[entry + 14],
|
||||||
|
ico[entry + 15],
|
||||||
|
]) as usize;
|
||||||
|
let Some(end) = offset.checked_add(size) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(data) = ico.get(offset..end) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
match decode_icon(&ico[entry..entry + 16], data) {
|
||||||
|
Ok(image) => {
|
||||||
|
let pixels = u64::from(image.width()) * u64::from(image.height());
|
||||||
|
if best
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|(best_pixels, _)| pixels > *best_pixels)
|
||||||
|
{
|
||||||
|
best = Some((pixels, image));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => errors.push(format!("frame {i}: {err}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
best.map(|(_, image)| image).ok_or_else(|| {
|
||||||
|
if errors.is_empty() {
|
||||||
|
"PE icon contains no complete frames".to_string()
|
||||||
|
} else {
|
||||||
|
format!("PE icon could not be decoded ({})", errors.join("; "))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_icon(entry: &[u8], data: &[u8]) -> Result<RgbaImage, String> {
|
||||||
|
if data.starts_with(b"\x89PNG\r\n\x1a\n") {
|
||||||
|
return image::ImageReader::with_format(io::Cursor::new(data), ImageFormat::Png)
|
||||||
|
.decode()
|
||||||
|
.map(DynamicImage::into_rgba8)
|
||||||
|
.map_err(|err| err.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let capacity = 22usize
|
||||||
|
.checked_add(data.len())
|
||||||
|
.ok_or_else(|| "frame is too large".to_string())?;
|
||||||
|
let data_len = u32::try_from(data.len()).map_err(|_| "frame is too large".to_string())?;
|
||||||
|
let mut ico = Vec::with_capacity(capacity);
|
||||||
|
ico.extend_from_slice(&[0, 0, 1, 0, 1, 0]);
|
||||||
|
ico.extend_from_slice(entry);
|
||||||
|
ico[14..18].copy_from_slice(&data_len.to_le_bytes());
|
||||||
|
ico[18..22].copy_from_slice(&22u32.to_le_bytes());
|
||||||
|
ico.extend_from_slice(data);
|
||||||
|
|
||||||
|
image::ImageReader::with_format(io::Cursor::new(ico), ImageFormat::Ico)
|
||||||
|
.decode()
|
||||||
|
.map(DynamicImage::into_rgba8)
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{largest_icon, thumbnail_from_args};
|
||||||
|
use image::{DynamicImage, ImageFormat, Rgb, RgbImage, Rgba, RgbaImage};
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_thumbnail_request_returns_an_error() {
|
||||||
|
let args = [OsString::from("output.png")];
|
||||||
|
assert!(thumbnail_from_args(args.into_iter()).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn largest_icon_skips_invalid_entries() {
|
||||||
|
let image = RgbaImage::from_pixel(2, 2, Rgba([1, 2, 3, 255]));
|
||||||
|
let mut png = Cursor::new(Vec::new());
|
||||||
|
DynamicImage::ImageRgba8(image)
|
||||||
|
.write_to(&mut png, ImageFormat::Png)
|
||||||
|
.unwrap();
|
||||||
|
let png = png.into_inner();
|
||||||
|
|
||||||
|
let mut ico = vec![0, 0, 1, 0, 2, 0];
|
||||||
|
let mut invalid_entry = [0; 16];
|
||||||
|
invalid_entry[8..12].copy_from_slice(&4u32.to_le_bytes());
|
||||||
|
invalid_entry[12..16].copy_from_slice(&u32::MAX.to_le_bytes());
|
||||||
|
ico.extend_from_slice(&invalid_entry);
|
||||||
|
|
||||||
|
let mut valid_entry = [0; 16];
|
||||||
|
valid_entry[..8].copy_from_slice(&[2, 2, 0, 0, 1, 0, 32, 0]);
|
||||||
|
valid_entry[8..12].copy_from_slice(&u32::try_from(png.len()).unwrap().to_le_bytes());
|
||||||
|
valid_entry[12..16].copy_from_slice(&38u32.to_le_bytes());
|
||||||
|
ico.extend_from_slice(&valid_entry);
|
||||||
|
ico.extend_from_slice(&png);
|
||||||
|
|
||||||
|
let decoded = largest_icon(&ico).expect("valid icon after malformed entry");
|
||||||
|
assert_eq!(decoded.dimensions(), (2, 2));
|
||||||
|
assert_eq!(decoded.get_pixel(0, 0), &Rgba([1, 2, 3, 255]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dib_and_mask_is_applied() {
|
||||||
|
let mut dib = vec![0; 40];
|
||||||
|
dib[0..4].copy_from_slice(&40u32.to_le_bytes());
|
||||||
|
dib[4..8].copy_from_slice(&2i32.to_le_bytes());
|
||||||
|
dib[8..12].copy_from_slice(&4i32.to_le_bytes());
|
||||||
|
dib[12..14].copy_from_slice(&1u16.to_le_bytes());
|
||||||
|
dib[14..16].copy_from_slice(&24u16.to_le_bytes());
|
||||||
|
|
||||||
|
// Two bottom-up BGR rows, padded to four-byte boundaries.
|
||||||
|
dib.extend_from_slice(&[0, 0, 255, 0, 0, 255, 0, 0]);
|
||||||
|
dib.extend_from_slice(&[0, 0, 255, 0, 0, 255, 0, 0]);
|
||||||
|
// The first mask row is the bottom image row; its first pixel is transparent.
|
||||||
|
dib.extend_from_slice(&[0b1000_0000, 0, 0, 0]);
|
||||||
|
dib.extend_from_slice(&[0, 0, 0, 0]);
|
||||||
|
|
||||||
|
let mut ico = vec![0, 0, 1, 0, 1, 0];
|
||||||
|
let mut entry = [0; 16];
|
||||||
|
entry[..8].copy_from_slice(&[2, 2, 0, 0, 1, 0, 24, 0]);
|
||||||
|
entry[8..12].copy_from_slice(&u32::try_from(dib.len()).unwrap().to_le_bytes());
|
||||||
|
entry[12..16].copy_from_slice(&22u32.to_le_bytes());
|
||||||
|
ico.extend_from_slice(&entry);
|
||||||
|
ico.extend_from_slice(&dib);
|
||||||
|
|
||||||
|
let decoded = largest_icon(&ico).expect("valid DIB icon");
|
||||||
|
assert_eq!(decoded.get_pixel(0, 0), &Rgba([255, 0, 0, 255]));
|
||||||
|
assert_eq!(decoded.get_pixel(0, 1), &Rgba([255, 0, 0, 0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rgb_png_icon_is_decoded() {
|
||||||
|
let image = RgbImage::from_pixel(2, 2, Rgb([1, 2, 3]));
|
||||||
|
let mut png = Cursor::new(Vec::new());
|
||||||
|
DynamicImage::ImageRgb8(image)
|
||||||
|
.write_to(&mut png, ImageFormat::Png)
|
||||||
|
.unwrap();
|
||||||
|
let png = png.into_inner();
|
||||||
|
|
||||||
|
let mut ico = vec![0, 0, 1, 0, 1, 0];
|
||||||
|
let mut entry = [0; 16];
|
||||||
|
entry[..8].copy_from_slice(&[2, 2, 0, 0, 1, 0, 24, 0]);
|
||||||
|
entry[8..12].copy_from_slice(&u32::try_from(png.len()).unwrap().to_le_bytes());
|
||||||
|
entry[12..16].copy_from_slice(&22u32.to_le_bytes());
|
||||||
|
ico.extend_from_slice(&entry);
|
||||||
|
ico.extend_from_slice(&png);
|
||||||
|
|
||||||
|
let decoded = largest_icon(&ico).expect("valid RGB PNG icon");
|
||||||
|
assert_eq!(decoded.get_pixel(0, 0), &Rgba([1, 2, 3, 255]));
|
||||||
|
}
|
||||||
|
}
|
||||||
17
justfile
17
justfile
|
|
@ -12,6 +12,10 @@ cargo-target-dir := env('CARGO_TARGET_DIR', 'target')
|
||||||
bin-src := cargo-target-dir / 'release' / name
|
bin-src := cargo-target-dir / 'release' / name
|
||||||
bin-dst := base-dir / 'bin' / name
|
bin-dst := base-dir / 'bin' / name
|
||||||
|
|
||||||
|
thumbnailer-bin-name := name + '-thumbnailer'
|
||||||
|
thumbnailer-bin-src := cargo-target-dir / 'release' / thumbnailer-bin-name
|
||||||
|
thumbnailer-bin-dst := base-dir / 'bin' / thumbnailer-bin-name
|
||||||
|
|
||||||
applet-name := name + '-applet'
|
applet-name := name + '-applet'
|
||||||
applet-src := cargo-target-dir / 'release' / applet-name
|
applet-src := cargo-target-dir / 'release' / applet-name
|
||||||
applet-dst := base-dir / 'bin' / applet-name
|
applet-dst := base-dir / 'bin' / applet-name
|
||||||
|
|
@ -24,6 +28,10 @@ metainfo := APPID + '.metainfo.xml'
|
||||||
metainfo-src := 'target/xdgen' / metainfo
|
metainfo-src := 'target/xdgen' / metainfo
|
||||||
metainfo-dst := clean(rootdir / prefix) / 'share' / 'metainfo' / metainfo
|
metainfo-dst := clean(rootdir / prefix) / 'share' / 'metainfo' / metainfo
|
||||||
|
|
||||||
|
thumbnailer := APPID + '.thumbnailer'
|
||||||
|
thumbnailer-src := 'res' / thumbnailer
|
||||||
|
thumbnailer-dst := clean(rootdir / prefix) / 'share' / 'thumbnailers' / thumbnailer
|
||||||
|
|
||||||
icons-src := 'res' / 'icons' / 'hicolor'
|
icons-src := 'res' / 'icons' / 'hicolor'
|
||||||
icons-dst := clean(rootdir / prefix) / 'share' / 'icons' / 'hicolor'
|
icons-dst := clean(rootdir / prefix) / 'share' / 'icons' / 'hicolor'
|
||||||
|
|
||||||
|
|
@ -45,14 +53,11 @@ clean-dist: clean clean-vendor
|
||||||
build-debug *args:
|
build-debug *args:
|
||||||
cargo build {{args}}
|
cargo build {{args}}
|
||||||
cargo build --package {{applet-name}} {{args}}
|
cargo build --package {{applet-name}} {{args}}
|
||||||
|
cargo build --package {{thumbnailer-bin-name}} {{args}}
|
||||||
|
|
||||||
# Compiles with release profile
|
# Compiles with release profile
|
||||||
build-release *args: (build-debug '--release' args)
|
build-release *args: (build-debug '--release' args)
|
||||||
|
|
||||||
# Compiles applet with release profile
|
|
||||||
build-release-applet *args:
|
|
||||||
cargo build --package {{applet-name}} --release {{args}}
|
|
||||||
|
|
||||||
# Compiles release profile with vendored dependencies
|
# Compiles release profile with vendored dependencies
|
||||||
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
|
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
|
||||||
|
|
||||||
|
|
@ -92,9 +97,11 @@ heaptrack *args:
|
||||||
# Installs files
|
# Installs files
|
||||||
install:
|
install:
|
||||||
install -Dm0755 {{bin-src}} {{bin-dst}}
|
install -Dm0755 {{bin-src}} {{bin-dst}}
|
||||||
|
install -Dm0755 {{thumbnailer-bin-src}} {{thumbnailer-bin-dst}}
|
||||||
install -Dm0755 {{applet-src}} {{applet-dst}}
|
install -Dm0755 {{applet-src}} {{applet-dst}}
|
||||||
install -Dm0644 {{desktop-src}} {{desktop-dst}}
|
install -Dm0644 {{desktop-src}} {{desktop-dst}}
|
||||||
install -Dm0644 {{metainfo-src}} {{metainfo-dst}}
|
install -Dm0644 {{metainfo-src}} {{metainfo-dst}}
|
||||||
|
install -Dm0644 {{thumbnailer-src}} {{thumbnailer-dst}}
|
||||||
for size in `ls {{icons-src}}`; do \
|
for size in `ls {{icons-src}}`; do \
|
||||||
install -Dm0644 "{{icons-src}}/$size/apps/{{APPID}}.svg" "{{icons-dst}}/$size/apps/{{APPID}}.svg"; \
|
install -Dm0644 "{{icons-src}}/$size/apps/{{APPID}}.svg" "{{icons-dst}}/$size/apps/{{APPID}}.svg"; \
|
||||||
done
|
done
|
||||||
|
|
@ -105,7 +112,7 @@ install-applet:
|
||||||
|
|
||||||
# Uninstalls installed files
|
# Uninstalls installed files
|
||||||
uninstall:
|
uninstall:
|
||||||
rm -f {{bin-dst}} {{applet-dst}}
|
rm -f {{bin-dst}} {{thumbnailer-bin-dst}} {{applet-dst}} {{thumbnailer-dst}}
|
||||||
|
|
||||||
# Vendor dependencies locally
|
# Vendor dependencies locally
|
||||||
vendor:
|
vendor:
|
||||||
|
|
|
||||||
4
res/com.system76.CosmicFiles.thumbnailer
Normal file
4
res/com.system76.CosmicFiles.thumbnailer
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[Thumbnailer Entry]
|
||||||
|
TryExec=cosmic-files-thumbnailer
|
||||||
|
Exec=cosmic-files-thumbnailer %o --size %s %i
|
||||||
|
MimeType=application/vnd.microsoft.portable-executable;application/x-msdownload;
|
||||||
|
|
@ -172,3 +172,25 @@ pub fn thumbnailer(mime: &Mime) -> Vec<Thumbnailer> {
|
||||||
let thumbnailer_cache = THUMBNAILER_CACHE.lock().unwrap();
|
let thumbnailer_cache = THUMBNAILER_CACHE.lock().unwrap();
|
||||||
thumbnailer_cache.get(mime)
|
thumbnailer_cache.get(mime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "desktop"))]
|
||||||
|
mod tests {
|
||||||
|
use super::Thumbnailer;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_expands_thumbnailer_arguments() {
|
||||||
|
let thumbnailer = Thumbnailer {
|
||||||
|
exec: "cosmic-files-thumbnailer %o --size %s %i".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = thumbnailer
|
||||||
|
.command(Path::new("input.exe"), Path::new("output.png"), 128)
|
||||||
|
.expect("valid thumbnailer command");
|
||||||
|
assert_eq!(command.get_program(), "cosmic-files-thumbnailer");
|
||||||
|
assert_eq!(
|
||||||
|
command.get_args().collect::<Vec<_>>(),
|
||||||
|
["output.png", "--size", "128", "input.exe"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue