use crate::daemon_status::{DaemonStatus, DaemonStatusManager};
use anyhow::Result;
use image::DynamicImage;
use ratatui_image::{picker::Picker, protocol::StatefulProtocol};
use std::path::{Path, PathBuf};
use tokio::sync::mpsc;
use tracing::{debug, info, warn};
pub struct App {
pub should_quit: bool,
pub config: crate::config::Config,
pub wallpapers: Vec<WallpaperItem>,
pub selected: usize,
pub view_mode: ViewMode,
pub status_message: Option<String>,
pub is_loading: bool,
pub error_message: Option<String>,
pub open_editor: bool,
pub daemon_status: Option<DaemonStatus>,
daemon_status_manager: DaemonStatusManager,
pub image_picker: Option<Picker>,
pub thumbnail_state: Option<StatefulProtocol>,
thumbnail_loaded_for: Option<usize>,
thumbnail_loading_for: Option<usize>,
image_rx: mpsc::Receiver<(usize, DynamicImage)>,
image_tx: mpsc::Sender<(usize, DynamicImage)>,
}
#[derive(Debug, Clone)]
pub struct WallpaperItem {
pub path: PathBuf,
pub name: String,
pub size: Option<u64>,
pub dimensions: Option<(u32, u32)>,
pub format: Option<String>,
pub is_current: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ViewMode {
Browse,
Preview,
Help,
}
#[allow(dead_code)]
impl App {
pub async fn new(config: crate::config::Config) -> Result<Self> {
debug!("🎨 Initializing TUI application");
let daemon_status_manager = DaemonStatusManager::new()?;
let image_picker = match Picker::from_query_stdio() {
Ok(picker) => {
info!("🖼️ Terminal graphics protocol detected: {:?}", picker.protocol_type());
Some(picker)
}
Err(e) => {
debug!("Terminal graphics not available: {}", e);
None
}
};
let (image_tx, image_rx) = mpsc::channel(4);
let mut app = Self {
should_quit: false,
config,
wallpapers: Vec::new(),
selected: 0,
view_mode: ViewMode::Browse,
status_message: Some("Loading wallpapers...".to_string()),
is_loading: true,
error_message: None,
open_editor: false,
daemon_status: None,
daemon_status_manager,
image_picker,
thumbnail_state: None,
thumbnail_loaded_for: None,
thumbnail_loading_for: None,
image_rx,
image_tx,
};
app.refresh_wallpapers().await?;
app.update_daemon_status().await?;
app.request_thumbnail();
app.is_loading = false;
app.status_message = Some(format!("Found {} wallpapers", app.wallpapers.len()));
Ok(app)
}
pub async fn refresh_wallpapers(&mut self) -> Result<()> {
debug!("Refreshing wallpaper collection");
self.is_loading = true;
self.error_message = None;
let wallpaper_dir = Path::new(&self.config.paths.local);
if !wallpaper_dir.exists() {
let error = format!("Wallpaper directory does not exist: {}", wallpaper_dir.display());
warn!("{}", error);
self.error_message = Some(error);
self.is_loading = false;
return Ok(());
}
let mut wallpapers = Vec::new();
self.collect_wallpapers(
wallpaper_dir,
&self.config.sources.local.formats,
&mut wallpapers,
self.config.sources.local.recursive,
)?;
wallpapers.sort_by(|a, b| a.name.cmp(&b.name));
self.wallpapers = wallpapers;
self.selected = 0;
debug!("📁 Loaded {} wallpapers", self.wallpapers.len());
Ok(())
}
fn collect_wallpapers(&self, dir: &Path, formats: &[String], wallpapers: &mut Vec<WallpaperItem>, recursive: bool) -> Result<()> {
let entries = std::fs::read_dir(dir).map_err(|e| anyhow::anyhow!("Failed to read directory {}: {}", dir.display(), e))?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(extension) = path.extension().and_then(|ext| ext.to_str())
&& formats.iter().any(|fmt| fmt.eq_ignore_ascii_case(extension))
{
let wallpaper_item = self.create_wallpaper_item(&path)?;
wallpapers.push(wallpaper_item);
}
} else if path.is_dir() && recursive {
self.collect_wallpapers(&path, formats, wallpapers, recursive)?;
}
}
Ok(())
}
fn create_wallpaper_item(&self, path: &Path) -> Result<WallpaperItem> {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("Unknown").to_string();
let size = std::fs::metadata(path).ok().map(|m| m.len());
let dimensions = imagesize::size(path).ok().map(|s| (s.width as u32, s.height as u32));
let format = path.extension().and_then(|ext| ext.to_str()).map(|ext| ext.to_uppercase());
Ok(WallpaperItem {
path: path.to_path_buf(),
name,
size,
dimensions,
format,
is_current: false, })
}
pub fn select_previous(&mut self) {
if !self.wallpapers.is_empty() {
self.selected = if self.selected == 0 {
self.wallpapers.len() - 1
} else {
self.selected - 1
};
self.request_thumbnail();
}
}
pub fn select_next(&mut self) {
if !self.wallpapers.is_empty() {
self.selected = (self.selected + 1) % self.wallpapers.len();
self.request_thumbnail();
}
}
pub fn request_thumbnail(&mut self) {
if self.thumbnail_loaded_for == Some(self.selected) {
return;
}
if self.thumbnail_loading_for == Some(self.selected) {
return;
}
if self.image_picker.is_none() {
return;
}
let Some(wallpaper) = self.wallpapers.get(self.selected) else {
return;
};
self.thumbnail_state = None;
self.thumbnail_loaded_for = None;
let index = self.selected;
let path = wallpaper.path.clone();
let tx = self.image_tx.clone();
self.thumbnail_loading_for = Some(index);
tokio::spawn(async move {
let load_result = tokio::task::spawn_blocking(move || image::ImageReader::open(&path).ok().and_then(|r| r.decode().ok())).await;
if let Ok(Some(img)) = load_result {
let _ = tx.send((index, img)).await;
}
});
}
pub fn poll_thumbnail(&mut self) {
while let Ok((index, dyn_img)) = self.image_rx.try_recv() {
if index == self.selected
&& let Some(picker) = &mut self.image_picker
{
self.thumbnail_state = Some(picker.new_resize_protocol(dyn_img));
self.thumbnail_loaded_for = Some(index);
debug!("Loaded thumbnail for index: {}", index);
}
if self.thumbnail_loading_for == Some(index) {
self.thumbnail_loading_for = None;
}
}
}
pub fn is_thumbnail_loading(&self) -> bool {
self.thumbnail_loading_for.is_some()
}
pub fn supports_images(&self) -> bool {
self.image_picker.is_some()
}
pub fn config_path(&self) -> std::path::PathBuf {
crate::config::Config::default_path()
}
pub fn selected_wallpaper(&self) -> Option<&WallpaperItem> {
self.wallpapers.get(self.selected)
}
pub async fn apply_selected_wallpaper(&mut self) -> Result<()> {
if let Some(wallpaper) = self.selected_wallpaper().cloned() {
debug!("🖼️ Setting wallpaper: {}", wallpaper.name);
self.status_message = Some("Applying wallpaper...".to_string());
match crate::wallpaper::apply_wallpaper(&wallpaper.path, &self.config).await {
Ok(()) => {
self.status_message = Some(format!("✅ Applied: {}", wallpaper.name));
for item in &mut self.wallpapers {
item.is_current = item.path == wallpaper.path;
}
}
Err(e) => {
let error = format!("❌ Failed to apply wallpaper: {}", e);
warn!("{}", error);
self.error_message = Some(error);
}
}
}
Ok(())
}
pub fn set_view_mode(&mut self, mode: ViewMode) {
debug!("Switching to view mode: {:?}", mode);
self.view_mode = mode;
}
pub fn quit(&mut self) {
debug!("🚪 Exiting TUI application");
self.should_quit = true;
}
pub fn clear_messages(&mut self) {
self.status_message = None;
self.error_message = None;
}
pub async fn update_daemon_status(&mut self) -> Result<()> {
self.daemon_status = self.daemon_status_manager.get_status().await?;
Ok(())
}
pub async fn is_daemon_running(&mut self) -> Result<bool> {
self.daemon_status_manager.is_daemon_running().await
}
pub fn daemon_time_remaining(&self) -> Option<String> {
self.daemon_status.as_ref().map(|s| s.time_remaining_formatted())
}
pub fn status_info(&self) -> String {
match &self.daemon_status {
Some(status) if status.is_stale() => "Daemon: Offline".to_string(),
Some(status) => format!("Daemon: {} remaining", status.time_remaining_formatted()),
None => "Daemon: Unknown".to_string(),
}
}
}
pub fn format_file_size(size: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
const THRESHOLD: f64 = 1024.0;
if size == 0 {
return "0 B".to_string();
}
let size_f = size as f64;
let unit_index = (size_f.log(THRESHOLD).floor() as usize).min(UNITS.len() - 1);
let value = size_f / THRESHOLD.powi(unit_index as i32);
if unit_index == 0 {
format!("{} {}", size, UNITS[unit_index])
} else {
format!("{:.1} {}", value, UNITS[unit_index])
}
}