Complete Clean Architecture migration

Phase 1-7: Full migration from src/app/ to Clean Architecture

BREAKING CHANGES:
- Removed src/app/ (old TEA-style implementation)
- Removed src/constant.rs (constants now local to modules)
- Removed deprecated canvas_to_image_coords functions

NEW STRUCTURE:
- src/ui/           - UI Layer (COSMIC interface)
- src/application/  - Application Layer (DocumentManager, Commands)
- src/domain/       - Domain Layer (Document types, Operations)
- src/infrastructure/ - Infrastructure Layer (Loaders, Cache, System)

FEATURES:
- DocumentManager as Single Source of Truth
- Command Pattern for all operations
- Model caching for render data (performance)
- Sync mechanism between DocumentManager and UI Model
- Wallpaper support (COSMIC, KDE, GNOME, feh)
- Thumbnail cache with disk persistence

IMPROVEMENTS:
- Warnings: 62 → 43 (-31%)
- Deprecated warnings: 2 → 0 (-100%)
- Code removed: src/app/ (~2000 lines), constant.rs, deprecated functions
- Better Locality of Reference (constants local to modules)
- Clean separation of concerns
- No circular dependencies

DOCUMENTATION:
- Updated AGENTS.md (100% migration status)
- Updated README.md (architecture section)
- Updated Workflow.md
- Added Migration-Plan.md with full completion summary

TESTS:
- All 41 tests passing
- Build successful (0 errors, 43 warnings)
- Release build verified

Migration Status:  100% Complete
This commit is contained in:
wfx 2026-02-03 08:43:21 +01:00
parent f8087a3c6a
commit fc73e4b76b
87 changed files with 9461 additions and 3324 deletions

9
src/infrastructure/cache/mod.rs vendored Normal file
View file

@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/infrastructure/cache/mod.rs
//
// Cache infrastructure: thumbnail and document caching.
pub mod thumbnail_cache;
// Re-export ThumbnailCache
pub use thumbnail_cache::ThumbnailCache;

View file

@ -0,0 +1,149 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/infrastructure/cache/thumbnail_cache.rs
//
// Disk cache for document thumbnails stored in ~/.cache/noctua/
use std::fs;
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use image::DynamicImage;
use sha2::{Digest, Sha256};
use cosmic::widget::image::Handle as ImageHandle;
use crate::domain::document::operations::render::create_image_handle_from_image;
/// Cache directory name under ~/.cache/ for thumbnail storage.
const CACHE_DIR: &str = "noctua";
/// File extension for cached thumbnails.
const THUMBNAIL_EXT: &str = "png";
/// Thumbnail cache manager for disk-based caching.
pub struct ThumbnailCache;
impl ThumbnailCache {
/// Load a thumbnail from disk cache.
/// Returns None if not cached or cache is invalid.
pub fn load(file_path: &Path, page: usize) -> Option<ImageHandle> {
let cache_path = Self::thumbnail_path(file_path, page)?;
log::debug!("Cache lookup: file={}, page={}", file_path.display(), page);
if !cache_path.exists() {
log::debug!(
"Thumbnail not found in cache: file={} page={}",
file_path.display(),
page
);
return None;
}
let img = image::open(&cache_path).ok()?;
log::debug!(
"Thumbnail loaded from cache: file={} page={}",
file_path.display(),
page
);
Some(create_image_handle_from_image(&img))
}
/// Save a thumbnail to disk cache.
pub fn save(file_path: &Path, page: usize, image: &DynamicImage) -> Option<()> {
let dir = Self::ensure_cache_dir()?;
let key = Self::cache_key(file_path, page)?;
let cache_path = dir.join(format!("{key}.{THUMBNAIL_EXT}"));
log::debug!(
"Saving thumbnail to cache: file={}, page={}, path={}",
file_path.display(),
page,
cache_path.display()
);
let file = fs::File::create(&cache_path).ok()?;
let writer = BufWriter::new(file);
let res = image.write_to(
&mut std::io::BufWriter::new(writer),
image::ImageFormat::Png,
);
match res {
Ok(()) => {
log::debug!(
"Thumbnail cached successfully: file={} page={}",
file_path.display(),
page
);
Some(())
}
Err(e) => {
log::warn!(
"Failed to cache thumbnail: file={} page={}: {}",
file_path.display(),
page,
e
);
None
}
}
}
/// Clear all cached thumbnails.
pub fn clear_cache() -> std::io::Result<()> {
if let Some(dir) = Self::cache_dir()
&& dir.exists()
{
fs::remove_dir_all(&dir)?;
}
Ok(())
}
/// Check if a thumbnail exists in cache.
#[allow(dead_code)]
pub fn has(file_path: &Path, page: usize) -> bool {
Self::thumbnail_path(file_path, page).is_some_and(|p| p.exists())
}
// Private helper methods
/// Get the cache directory path (~/.cache/noctua/).
fn cache_dir() -> Option<PathBuf> {
dirs::cache_dir().map(|p| p.join(CACHE_DIR))
}
/// Ensure the cache directory exists.
fn ensure_cache_dir() -> Option<PathBuf> {
let dir = Self::cache_dir()?;
fs::create_dir_all(&dir).ok()?;
Some(dir)
}
/// Generate a cache key from file path, modification time, and page number.
/// Format: sha256(path + mtime + page)
fn cache_key(file_path: &Path, page: usize) -> Option<String> {
let metadata = fs::metadata(file_path).ok()?;
let mtime = metadata
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs();
let mut hasher = Sha256::new();
hasher.update(file_path.to_string_lossy().as_bytes());
hasher.update(mtime.to_le_bytes());
hasher.update(page.to_le_bytes());
let hash = hasher.finalize();
Some(format!("{hash:x}"))
}
/// Get the full path for a cached thumbnail.
fn thumbnail_path(file_path: &Path, page: usize) -> Option<PathBuf> {
let dir = Self::cache_dir()?;
let key = Self::cache_key(file_path, page)?;
Some(dir.join(format!("{key}.{THUMBNAIL_EXT}")))
}
}

View file

@ -0,0 +1,189 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/infrastructure/filesystem/file_ops.rs
//
// File system operations for document handling.
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::anyhow;
use crate::domain::document::core::content::{DocumentContent, DocumentKind};
use crate::domain::document::types::raster::RasterDocument;
#[cfg(feature = "vector")]
use crate::domain::document::types::vector::VectorDocument;
#[cfg(feature = "portable")]
use crate::domain::document::types::portable::PortableDocument;
/// Open a document from a file path and dispatch to the correct type.
///
/// Raster formats are delegated to the `image` crate, which decides
/// based on enabled codecs (e.g. default-formats).
pub fn open_document(path: &Path) -> anyhow::Result<DocumentContent> {
let kind = DocumentKind::from_path(path)
.ok_or_else(|| anyhow!("Unsupported document type: {}", path.display()))?;
let content = match kind {
DocumentKind::Raster => {
let raster = RasterDocument::open(path)?;
DocumentContent::Raster(raster)
}
#[cfg(feature = "vector")]
DocumentKind::Vector => {
let vector = VectorDocument::open(path)?;
DocumentContent::Vector(vector)
}
#[cfg(feature = "portable")]
DocumentKind::Portable => {
let portable = PortableDocument::open(path)?;
DocumentContent::Portable(portable)
}
#[cfg(not(any(feature = "vector", feature = "portable")))]
_ => return Err(anyhow!("No document features enabled")),
};
Ok(content)
}
/// Collect all supported document files from a directory, sorted alphabetically.
///
/// This scans the directory and returns a list of files that are recognized as
/// supported document types (images, PDFs, SVGs, etc.).
pub fn collect_supported_files(dir: &Path) -> Vec<PathBuf> {
let mut entries: Vec<PathBuf> = Vec::new();
if let Ok(read_dir) = fs::read_dir(dir) {
for entry in read_dir.flatten() {
let path = entry.path();
// Only keep regular files that are recognized as supported documents.
if path.is_file() && DocumentKind::from_path(&path).is_some() {
entries.push(path);
}
}
}
entries.sort();
entries
}
// ---------------------------------------------------------------------------
// File metadata helpers
// ---------------------------------------------------------------------------
/// Retrieve the file size in bytes. Returns 0 if the file cannot be accessed.
pub fn file_size(path: &Path) -> u64 {
fs::metadata(path).map(|m| m.len()).unwrap_or(0)
}
/// Read raw bytes from a file for metadata extraction (e.g., EXIF).
/// Returns None if the file cannot be read.
pub fn read_file_bytes(path: &Path) -> Option<Vec<u8>> {
fs::read(path).ok()
}
// ---------------------------------------------------------------------------
// DEPRECATED FUNCTIONS
// ---------------------------------------------------------------------------
// The following functions have been replaced by DocumentManager and are
// commented out to avoid AppModel dependencies.
//
// Instead of using these functions directly, use:
// - DocumentManager::open_document() for opening files
// - DocumentManager::next_document() / previous_document() for navigation
// - Application commands for operations like crop, save, etc.
// ---------------------------------------------------------------------------
/*
/// Open the initial path passed on the command line.
///
/// DEPRECATED: Use DocumentManager::open_document() instead.
pub fn open_initial_path(model: &mut AppModel, path: &PathBuf) {
if path.is_dir() {
open_from_directory(model, path);
} else {
open_single_file(model, path);
}
}
/// Open the first supported document from the given directory.
///
/// DEPRECATED: Use DocumentManager::open_document() instead.
pub fn open_from_directory(model: &mut AppModel, dir: &Path) {
let entries = collect_supported_files(dir);
if entries.is_empty() {
model.set_error(format!(
"No supported documents found in directory: {}",
dir.display()
));
return;
}
let first = entries[0].clone();
model.folder_entries = entries;
model.current_index = Some(0);
load_document_into_model(model, &first);
}
/// Open a single file.
///
/// DEPRECATED: Use DocumentManager::open_document() instead.
pub fn open_single_file(model: &mut AppModel, path: &Path) {
load_document_into_model(model, path);
if model.document.is_some()
&& let Some(parent) = path.parent()
{
refresh_folder_entries(model, parent, path);
}
}
/// Load a document into the model.
///
/// DEPRECATED: Use DocumentManager methods instead.
fn load_document_into_model(model: &mut AppModel, path: &Path) {
// Implementation omitted - use DocumentManager instead
}
/// Refresh folder entries.
///
/// DEPRECATED: DocumentManager handles this automatically.
pub fn refresh_folder_entries(model: &mut AppModel, folder: &Path, current: &Path) {
// Implementation omitted - use DocumentManager instead
}
/// Navigate to the next document.
///
/// DEPRECATED: Use DocumentManager::next_document() instead.
pub fn navigate_next(model: &mut AppModel) {
// Implementation omitted - use DocumentManager instead
}
/// Navigate to the previous document.
///
/// DEPRECATED: Use DocumentManager::previous_document() instead.
pub fn navigate_prev(model: &mut AppModel) {
// Implementation omitted - use DocumentManager instead
}
/// Apply crop operation.
///
/// DEPRECATED: Use CropDocumentCommand instead.
pub fn apply_crop(
crop_selection: &CropSelection,
doc: &DocumentContent,
current_path: &Path,
canvas_size: cosmic::iced::Size,
image_size: cosmic::iced::Size,
scale: f32,
pan_x: f32,
pan_y: f32,
view_mode: &ViewMode,
) -> Result<PathBuf, String> {
// Implementation omitted - use CropDocumentCommand instead
Err("Deprecated function - use CropDocumentCommand".to_string())
}
*/

View file

@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/infrastructure/filesystem/mod.rs
//
// Filesystem operations: file I/O, folder scanning, and file watching.
pub mod file_ops;
// TODO: Re-implement these helpers without UI dependencies
// pub use file_ops::{file_size, read_file_bytes};

View file

@ -0,0 +1,148 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/infrastructure/loaders/document_loader.rs
//
// Document loader trait and factory for loading documents from files.
use std::path::Path;
use crate::domain::document::core::content::{DocumentContent, DocumentKind};
use crate::domain::document::core::document::DocResult;
use super::raster_loader::RasterLoader;
#[cfg(feature = "vector")]
use super::svg_loader::SvgLoader;
#[cfg(feature = "portable")]
use super::pdf_loader::PdfLoader;
/// Trait for loading documents from files.
///
/// Implementations handle specific document formats (raster, vector, portable).
pub trait DocumentLoader {
/// Load a document from a file path.
fn load(&self, path: &Path) -> DocResult<DocumentContent>;
/// Check if this loader supports the given file.
fn supports(&self, path: &Path) -> bool;
}
/// Document loader factory.
///
/// Detects the document format and delegates to the appropriate loader.
pub struct DocumentLoaderFactory;
impl DocumentLoaderFactory {
/// Create a new document loader factory.
#[must_use]
pub fn new() -> Self {
Self
}
/// Load a document from a file, automatically detecting the format.
///
/// # Errors
///
/// Returns an error if:
/// - The file format is not supported
/// - The file cannot be read
/// - The document is malformed
pub fn load(&self, path: &Path) -> DocResult<DocumentContent> {
let kind = DocumentKind::from_path(path).ok_or_else(|| {
anyhow::anyhow!(
"Unsupported file format: {}",
path.extension()
.and_then(|e| e.to_str())
.unwrap_or("unknown")
)
})?;
match kind {
DocumentKind::Raster => {
let loader = RasterLoader;
loader.load(path)
}
#[cfg(feature = "vector")]
DocumentKind::Vector => {
let loader = SvgLoader;
loader.load(path)
}
#[cfg(feature = "portable")]
DocumentKind::Portable => {
let loader = PdfLoader;
loader.load(path)
}
#[cfg(not(any(feature = "vector", feature = "portable")))]
_ => Err(anyhow::anyhow!(
"No document loaders available (check feature flags)"
)),
}
}
/// Detect the document kind from a file path.
#[must_use]
pub fn detect_kind(&self, path: &Path) -> Option<DocumentKind> {
DocumentKind::from_path(path)
}
/// Check if a file is supported by any loader.
#[must_use]
pub fn is_supported(&self, path: &Path) -> bool {
DocumentKind::from_path(path).is_some()
}
}
impl Default for DocumentLoaderFactory {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_factory_creation() {
let factory = DocumentLoaderFactory::new();
assert!(std::ptr::eq(&factory, &factory)); // Just a dummy test
}
#[test]
fn test_detect_kind() {
let factory = DocumentLoaderFactory::new();
assert_eq!(
factory.detect_kind(Path::new("test.png")),
Some(DocumentKind::Raster)
);
assert_eq!(
factory.detect_kind(Path::new("test.jpg")),
Some(DocumentKind::Raster)
);
#[cfg(feature = "vector")]
{
assert_eq!(
factory.detect_kind(Path::new("test.svg")),
Some(DocumentKind::Vector)
);
}
#[cfg(feature = "portable")]
{
assert_eq!(
factory.detect_kind(Path::new("test.pdf")),
Some(DocumentKind::Portable)
);
}
assert_eq!(factory.detect_kind(Path::new("test.txt")), None);
}
#[test]
fn test_is_supported() {
let factory = DocumentLoaderFactory::new();
assert!(factory.is_supported(Path::new("test.png")));
assert!(!factory.is_supported(Path::new("test.txt")));
}
}

View file

@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/infrastructure/loaders/mod.rs
//
// Document loaders for various formats.
pub mod document_loader;
pub mod raster_loader;
#[cfg(feature = "vector")]
pub mod svg_loader;
#[cfg(feature = "portable")]
pub mod pdf_loader;
// Re-export main types
pub use document_loader::DocumentLoaderFactory;

View file

@ -0,0 +1,50 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/infrastructure/loaders/pdf_loader.rs
//
// Loader for PDF portable documents.
use std::path::Path;
use crate::domain::document::core::content::DocumentContent;
use crate::domain::document::core::document::DocResult;
use crate::domain::document::types::portable::PortableDocument;
use crate::infrastructure::loaders::document_loader::DocumentLoader;
/// Loader for PDF portable documents.
pub struct PdfLoader;
impl DocumentLoader for PdfLoader {
fn load(&self, path: &Path) -> DocResult<DocumentContent> {
let document = PortableDocument::open(path)
.map_err(|e| anyhow::anyhow!("Failed to load PDF document: {e}"))?;
Ok(DocumentContent::Portable(document))
}
fn supports(&self, path: &Path) -> bool {
if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
ext_str == "pdf"
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_supports() {
let loader = PdfLoader;
assert!(loader.supports(Path::new("test.pdf")));
assert!(loader.supports(Path::new("test.PDF")));
assert!(loader.supports(Path::new("document.pdf")));
assert!(!loader.supports(Path::new("test.png")));
assert!(!loader.supports(Path::new("test.svg")));
assert!(!loader.supports(Path::new("test.jpg")));
assert!(!loader.supports(Path::new("test.txt")));
}
}

View file

@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/infrastructure/loaders/raster_loader.rs
//
// Loader for raster image documents (PNG, JPEG, WebP, etc.).
use std::path::Path;
use crate::domain::document::core::content::DocumentContent;
use crate::domain::document::core::document::DocResult;
use crate::domain::document::types::raster::RasterDocument;
use crate::infrastructure::loaders::document_loader::DocumentLoader;
/// Loader for raster image documents.
pub struct RasterLoader;
impl DocumentLoader for RasterLoader {
fn load(&self, path: &Path) -> DocResult<DocumentContent> {
let document = RasterDocument::open(path)
.map_err(|e| anyhow::anyhow!("Failed to load raster document: {e}"))?;
Ok(DocumentContent::Raster(document))
}
fn supports(&self, path: &Path) -> bool {
use cosmic::iced_renderer::graphics::image::image_rs::ImageFormat;
ImageFormat::from_path(path).is_ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_supports() {
let loader = RasterLoader;
assert!(loader.supports(Path::new("test.png")));
assert!(loader.supports(Path::new("test.jpg")));
assert!(loader.supports(Path::new("test.jpeg")));
assert!(loader.supports(Path::new("test.webp")));
assert!(!loader.supports(Path::new("test.pdf")));
assert!(!loader.supports(Path::new("test.svg")));
}
}

View file

@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/infrastructure/loaders/svg_loader.rs
//
// Loader for SVG vector documents.
use std::path::Path;
use crate::domain::document::core::content::DocumentContent;
use crate::domain::document::core::document::DocResult;
use crate::domain::document::types::vector::VectorDocument;
use crate::infrastructure::loaders::document_loader::DocumentLoader;
/// Loader for SVG vector documents.
pub struct SvgLoader;
impl DocumentLoader for SvgLoader {
fn load(&self, path: &Path) -> DocResult<DocumentContent> {
let document = VectorDocument::open(path)
.map_err(|e| anyhow::anyhow!("Failed to load SVG document: {e}"))?;
Ok(DocumentContent::Vector(document))
}
fn supports(&self, path: &Path) -> bool {
if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
ext_str == "svg" || ext_str == "svgz"
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_supports() {
let loader = SvgLoader;
assert!(loader.supports(Path::new("test.svg")));
assert!(loader.supports(Path::new("test.SVG")));
assert!(loader.supports(Path::new("test.svgz")));
assert!(!loader.supports(Path::new("test.png")));
assert!(!loader.supports(Path::new("test.pdf")));
assert!(!loader.supports(Path::new("test.jpg")));
}
}

13
src/infrastructure/mod.rs Normal file
View file

@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/infrastructure/mod.rs
//
// Infrastructure layer: external dependencies, loaders, cache, and filesystem.
pub mod cache;
pub mod filesystem;
pub mod loaders;
pub mod system;
// Re-export loader factory
#[allow(unused_imports)]
pub use loaders::DocumentLoaderFactory;

View file

@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/infrastructure/system/mod.rs
//
// System integration: wallpaper, desktop environment utilities.
pub mod wallpaper;
// Re-export wallpaper function
pub use wallpaper::set_as_wallpaper;

View file

@ -0,0 +1,159 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/infrastructure/system/wallpaper.rs
//
// Set desktop wallpaper across different desktop environments.
use std::path::Path;
/// Set an image as desktop wallpaper using multiple fallback methods.
///
/// Attempts the following methods in order:
/// 1. COSMIC Desktop (direct config file modification)
/// 2. wallpaper crate (KDE, XFCE, Windows, macOS)
/// 3. gsettings (GNOME)
/// 4. feh (tiling window managers)
pub fn set_as_wallpaper(path: &Path) {
// Canonicalize to absolute path.
let abs_path = match path.canonicalize() {
Ok(p) => p,
Err(e) => {
log::error!("Failed to canonicalize path {}: {}", path.display(), e);
return;
}
};
let Some(path_str) = abs_path.to_str() else {
log::error!("Invalid UTF-8 in path: {}", abs_path.display());
return;
};
log::info!("Attempting to set wallpaper: {path_str}");
// Method 1: Try COSMIC Desktop (direct config file modification).
if try_cosmic_wallpaper(path_str) {
return;
}
// Method 2: Try wallpaper crate (supports KDE, XFCE, Windows, macOS).
if try_wallpaper_crate(path_str) {
return;
}
// Method 3: Try GNOME via gsettings.
if try_gsettings_wallpaper(path_str) {
return;
}
// Method 4: Try feh (common on tiling WMs like i3, sway).
if try_feh_wallpaper(path_str) {
return;
}
log::error!("All methods failed to set wallpaper");
}
/// Try setting wallpaper via COSMIC config file.
fn try_cosmic_wallpaper(path_str: &str) -> bool {
let Some(home) = dirs::home_dir() else {
return false;
};
let cosmic_config = home.join(".config/cosmic/com.system76.CosmicBackground/v1/all");
if !cosmic_config.exists() {
return false;
}
let config_content = format!(
r#"(
output: "all",
source: Path("{path_str}"),
filter_by_theme: true,
rotation_frequency: 300,
filter_method: Lanczos,
scaling_mode: Zoom,
sampling_method: Alphanumeric,
)"#
);
match std::fs::write(&cosmic_config, config_content) {
Ok(()) => {
log::info!("Wallpaper set via COSMIC config");
true
}
Err(e) => {
log::warn!("Failed to write COSMIC config: {e}");
false
}
}
}
/// Try setting wallpaper via wallpaper crate.
fn try_wallpaper_crate(path_str: &str) -> bool {
match wallpaper::set_from_path(path_str) {
Ok(()) => {
log::info!("Wallpaper set via wallpaper crate");
true
}
Err(e) => {
log::warn!("wallpaper crate failed: {e}");
false
}
}
}
/// Try setting wallpaper via GNOME gsettings.
fn try_gsettings_wallpaper(path_str: &str) -> bool {
let uri = format!("file://{path_str}");
let output = match std::process::Command::new("gsettings")
.args(["set", "org.gnome.desktop.background", "picture-uri", &uri])
.output()
{
Ok(o) => o,
Err(e) => {
log::warn!("gsettings command failed: {e}");
return false;
}
};
if !output.status.success() {
log::warn!(
"gsettings failed: {}",
String::from_utf8_lossy(&output.stderr)
);
return false;
}
log::info!("Wallpaper set via gsettings");
// Also set dark mode wallpaper.
let _ = std::process::Command::new("gsettings")
.args([
"set",
"org.gnome.desktop.background",
"picture-uri-dark",
&uri,
])
.output();
true
}
/// Try setting wallpaper via feh.
fn try_feh_wallpaper(path_str: &str) -> bool {
let Ok(output) = std::process::Command::new("feh")
.args(["--bg-scale", path_str])
.output()
else {
log::warn!("feh not available");
return false;
};
if output.status.success() {
log::info!("Wallpaper set via feh");
true
} else {
log::warn!("feh failed");
false
}
}