//! Files owns one serialized async session. UI callbacks only capture input and //! schedule work; protocol, database, dialogs and disk I/O never block Slint. use crate::date_format::{self, DateFormats, DateStyle}; use crate::document_preview::{Preview, preview}; use crate::documents::safe_name; use crate::{AppWindow, FileRow, FilesUi, InboxState, OperationRow}; use flectar_mail_core::files::{ self, FileClient, service::{ Entry, FilesService as Directory, Output as ServiceOutput, Preview as ServicePreview, }, }; use slint::{ ComponentHandle, Model, ModelRc, Rgba8Pixel, SharedPixelBuffer, SharedString, VecModel, }; use std::{cell::RefCell, rc::Rc, sync::Arc}; use tokio::sync::Mutex; struct SyncWorker(tokio::task::JoinHandle<()>); impl Drop for SyncWorker { fn drop(&mut self) { self.0.abort(); } } #[cfg(mobile)] static SUSPEND: std::sync::OnceLock> = std::sync::OnceLock::new(); #[cfg(mobile)] pub(crate) fn suspend() { crate::pdf_preview::cancel_active(); if let Some(suspend) = SUSPEND.get() { suspend(); } } pub(crate) fn register( app: &AppWindow, state: &Rc>, runtime: &Rc, documents: Arc, scratch_root: std::path::PathBuf, ) { app.global::().set_pdf_supported(true); #[cfg(mobile)] { let weak = app.as_weak(); let _ = SUSPEND.set(Box::new(move || { let _ = weak.upgrade_in_event_loop(|app| { let ui = app.global::(); ui.invoke_command("release-preview".into(), "".into(), "".into()); ui.set_preview_open(false); }); })); crate::platform::install_preview_lifecycle(); } let weak = app.as_weak(); date_format::on_change(move || { let Some(app) = weak.upgrade() else { return; }; let rows = app.global::().get_rows(); let formats = date_format::current(); for index in 0..rows.row_count() { if let Some(mut row) = rows.row_data(index) { let date = file_date(&formats, &row.iso_date); if row.date != date { row.date = date; rows.set_row_data(index, row); } } } }); let view_active = Arc::new(std::sync::atomic::AtomicBool::new(false)); let directory = Arc::new(Mutex::new(( (0usize, 0u64), Directory { attachments: true, ..Default::default() }, ))); let pdf_document = Arc::new(std::sync::Mutex::new(None::>>)); let profile_pdf = pdf_document.clone(); let weak = app.as_weak(); let inbox = Rc::clone(state); let handle = runtime.handle().clone(); let worker = Rc::new(RefCell::new( None::<(Arc, SyncWorker)>, )); let preview_pending = Arc::new(std::sync::atomic::AtomicBool::new(false)); let pending = Rc::new(RefCell::new(None::)); let generation = Arc::new(std::sync::atomic::AtomicU64::new(0)); let account_epoch = Rc::new(std::cell::Cell::new(0_u64)); let account_state = state.clone(); let account_pending = pending.clone(); let account_generation = generation.clone(); let account_pdf = pdf_document.clone(); let epoch = account_epoch.clone(); let previous_accounts = Rc::new(RefCell::new(Vec::::new())); let window = app.as_weak(); app.global::().on_context_changed(move || { let ids = account_state .borrow() .connected_accounts .iter() .map(|a| a.id) .collect::>(); if *previous_accounts.borrow() == ids { return; } *previous_accounts.borrow_mut() = ids; epoch.set(epoch.get().wrapping_add(1)); account_generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst); if let Some(task) = account_pending.borrow_mut().take() { task.abort(); } *account_pdf.lock().unwrap() = None; if let Some(app) = window.upgrade() { reset_profile(&app.global::()); } }); let profile_pending = pending.clone(); let profile_generation = generation.clone(); let profile_ui = weak.clone(); let worker_timer = slint::Timer::default(); let worker_state = inbox.clone(); let worker_slot = worker.clone(); let worker_runtime = handle.clone(); worker_timer.start( slint::TimerMode::Repeated, std::time::Duration::from_millis(500), move || { if profile_ui .upgrade() .is_none_or(|app| app.get_active_view() != "files") { return; } let core = worker_state.borrow().core.as_ref().map(|c| c.file_core()); if let Some(core) = core { if worker_slot .borrow() .as_ref() .is_none_or(|(prior, _)| !Arc::ptr_eq(prior, &core)) { if worker_slot.borrow().is_some() { profile_generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst); if let Some(task) = profile_pending.borrow_mut().take() { task.abort(); } if let Some(app) = profile_ui.upgrade() { reset_profile(&app.global::()); *profile_pdf.lock().unwrap() = None; } } *worker_slot.borrow_mut() = Some(( core.clone(), SyncWorker(worker_runtime.spawn(files::sync::run((*core).clone()))), )); } } else if worker_slot.borrow_mut().take().is_some() { profile_generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst); if let Some(task) = profile_pending.borrow_mut().take() { task.abort(); } if let Some(app) = profile_ui.upgrade() { reset_profile(&app.global::()); *profile_pdf.lock().unwrap() = None; } } }, ); let progress = Arc::new(files::progress::Progress::default()); let progress_timer = slint::Timer::default(); let observed = progress.clone(); let progress_ui = weak.clone(); progress_timer.start( slint::TimerMode::Repeated, std::time::Duration::from_millis(100), move || { if let Some(app) = progress_ui.upgrade() { let ui = app.global::(); if ui.get_busy() { let p = observed.snapshot(); let phase = match p.phase { 1 => "Uploading", 2 => "Downloading", 3 => "Copying", _ => "Working", }; ui.set_transfer_status( if p.phase == 0 { String::new() } else if p.total > 0 { format!("{phase} · {} of {}", size(p.done), size(p.total)) } else { format!("{phase} · {}", size(p.done)) } .into(), ); } } }, ); app.global::().on_command(move |action, a, b| { let _keep_worker_timer_alive = (&worker_timer,&progress_timer); let Some(app) = weak.upgrade() else { return; }; let ui = app.global::(); if action.as_str() == "load" { view_active.store(true, std::sync::atomic::Ordering::SeqCst); } if action.as_str() == "release-view" { view_active.store(false, std::sync::atomic::Ordering::SeqCst); // The sync worker only refreshes rebuildable projections. A user // transfer is a separate task and is allowed to finish. worker.borrow_mut().take(); if preview_pending.swap(false, std::sync::atomic::Ordering::SeqCst) { generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst); if let Some(task) = pending.borrow_mut().take() { task.abort(); } ui.set_busy(false); } *pdf_document.lock().unwrap() = None; ui.set_pdf_pages(0); ui.set_preview_image(slint::Image::default()); ui.set_preview_text("".into()); ui.set_preview_open(false); ui.set_list_loading(false); ui.set_rows(Default::default()); ui.set_operations(Default::default()); let directory = directory.clone(); let generation = generation.clone(); let current = generation.load(std::sync::atomic::Ordering::SeqCst); let view_active = view_active.clone(); handle.spawn(async move { let mut workspace = directory.lock().await; if !view_active.load(std::sync::atomic::Ordering::SeqCst) && generation.load(std::sync::atomic::Ordering::SeqCst) == current { workspace.1.entries = Vec::new(); workspace.1.client = None; workspace.1.mail_search = None; } }); return; } if action.as_str() == "release-preview" { if preview_pending.swap(false, std::sync::atomic::Ordering::SeqCst) { generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst); if let Some(task) = pending.borrow_mut().take() { task.abort(); } ui.set_busy(false); } *pdf_document.lock().unwrap() = None; ui.set_pdf_pages(0); ui.set_preview_image(slint::Image::default()); ui.set_preview_text("".into()); return; } if action.as_str() == "cancel" { generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst); if let Some(task) = pending.borrow_mut().take() { task.abort(); } ui.set_busy(false); ui.set_list_loading(false); if preview_pending.swap(false, std::sync::atomic::Ordering::SeqCst) { ui.set_status("".into()); return; } ui.set_selected(-1); ui.set_can_create(false); ui.set_can_more(false); ui.set_status( "Cancelled. Refresh storage before retrying; a server change may have completed." .into(), ); return; } if ui.get_busy() { return; } let Some(core) = inbox.borrow().core.as_ref().map(|c| c.file_core()) else { ui.set_status("Mail is still starting. Try again shortly.".into()); return; }; let identity = (Arc::as_ptr(&core) as usize, account_epoch.get()); if worker .borrow() .as_ref() .is_none_or(|(prior, _)| !Arc::ptr_eq(prior, &core)) { *worker.borrow_mut() = Some(( core.clone(), SyncWorker(handle.spawn(files::sync::run((*core).clone()))), )); } if matches!(action.as_str(), "scope" | "account" | "account-id" | "space" | "load" | "search" | "filter" | "refresh" | "enter" | "up" | "attachment-source") { ui.set_dialog("".into()); ui.set_preview_open(false); ui.set_preview_image(slint::Image::default()); ui.set_pdf_pages(0); *pdf_document.lock().unwrap() = None; } if matches!(action.as_str(), "preview" | "details" | "notifications" | "clear-notifications") { ui.set_preview_image(slint::Image::default()); ui.set_preview_text("".into()); ui.set_preview_is_image(false); ui.set_activity_open(matches!(action.as_str(), "notifications" | "clear-notifications")); ui.set_preview_open(true); } if matches!(action.as_str(), "preview" | "details" | "notifications" | "clear-notifications") { ui.set_pdf_pages(0); ui.set_pdf_zoom(100); *pdf_document.lock().unwrap() = None; } let pdf_source = pdf_document.lock().unwrap().clone(); let pdf_document = pdf_document.clone(); let selected = ui.get_selected(); let collision = ui.get_collision(); let case_insensitive = ui.get_case_insensitive(); let action = action.to_string(); let list_loading = matches!( action.as_str(), "scope" | "account" | "account-id" | "space" | "load" | "search" | "filter" | "refresh" | "enter" | "up" | "attachment-source" | "connect" ); let a = if action.as_str()=="filter" { let size = |s:slint::SharedString|->Option {s.parse().ok()}; let min=ui.get_min_size();let max=ui.get_max_size(); if (!min.is_empty()&&size(min.clone()).is_none())||(!max.is_empty()&&size(max.clone()).is_none()) { ui.set_status("Enter file sizes as whole bytes between 0 and 4294967295.".into());return; } serde_json::json!({"nodeType":match ui.get_file_kind(){1=>Some("file"),2=>Some("directory"),_=>None},"minSize":size(min),"maxSize":size(max),"sort":match ui.get_file_sort(){1=>"size",2=>"nodeType",_=>"name"},"descending":ui.get_descending()}).to_string() } else {a.to_string()}; let b = b.to_string(); let documents = documents.clone(); let scratch_root = scratch_root.clone(); let directory = Arc::clone(&directory); let weak = weak.clone(); progress.reset(); ui.set_transfer_status("".into()); let progress=progress.clone(); preview_pending.store(matches!(action.as_str(), "preview" | "pdf-page" | "details" | "notifications" | "clear-notifications"), std::sync::atomic::Ordering::SeqCst); let preview_pending = preview_pending.clone(); ui.set_list_loading(list_loading); ui.set_busy(true); ui.set_status("".into()); let current_generation = generation.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1; let generation = Arc::clone(&generation); let task = handle.spawn(async move { let mut workspace = directory.lock().await; if workspace.0!=identity {workspace.0=identity;workspace.1=Directory{attachments:true,..Default::default()};} let dir=&mut workspace.1; dir.collision = match collision { 1 => files::CollisionPolicy::Rename, 2 => files::CollisionPolicy::Replace, 3 => files::CollisionPolicy::Newest, _ => files::CollisionPolicy::Reject, }; dir.case_insensitive = case_insensitive; let mut scratch = None; let selection = if action == "download" { let _ = tokio::fs::create_dir_all(&scratch_root).await; match tempfile::tempdir_in(&scratch_root) { Ok(dir) => { let path = dir.path().join("download"); scratch = Some(dir); Ok(Some(crate::documents::ImportedDocument::source(path))) } Err(e) => Err(e.to_string()), } } else if matches!(action.as_str(), "upload" | "replace") { documents.import().await } else { Ok(None) }; let export_path = selection.as_ref().ok().and_then(|v| v.as_ref()).map(|v| v.path().to_owned()); let mut result = match selection { Err(error) => Err(error), Ok(None) if matches!(action.as_str(), "upload" | "replace") => { Ok(ServiceOutput::Status("File selection cancelled.".into())) } Ok(_) if action == "pdf-page" => Ok(ServiceOutput::Status(String::new())), Ok(document) => { let name = document .as_ref() .filter(|_| matches!(action.as_str(), "upload" | "replace")) .and_then(|document| document.name()); let path = document.as_ref().map(|document| document.path().to_owned()); files::progress::track(progress, dir.execute(&core, &action, &a, &b, selected, path, name.as_deref())).await } }; if action == "download" && result.is_ok() && let Some(path) = export_path { let name = dir .selected(selected) .map(|e| match e { Entry::Attachment(a) => a.filename, Entry::Remote(n) => n.name, }) .unwrap_or_else(|_| "download".into()); let dialog = crate::documents::download_dialog_for(&core).await; result = documents.export_with(path, safe_name(&name), dialog).await.map(|saved| { ServiceOutput::Status( if saved { "File downloaded." } else { "Export cancelled; the cached file is still available." } .into(), ) }); } drop(scratch); let result = if action == "pdf-page" { match (pdf_source, a.parse::(), b.parse::()) { (Some(data), Ok(index), Ok(zoom)) => crate::pdf_preview::render(data.clone(), index, zoom).await .map(|page| Output::Preview(Preview::Pdf(data, page))), _ => Err("Select a PDF to preview.".into()), } } else { match result { Ok(ServiceOutput::Operations(rows)) => Ok(Output::Operations(rows)), Ok(ServiceOutput::Status(s)) => Ok(Output::Status(s)), Ok(ServiceOutput::Preview(ServicePreview::Text(s))) => { Ok(Output::Preview(Preview::Text(s))) } Ok(ServiceOutput::Preview(ServicePreview::Data(data, media))) => { preview(data, &media).await.map(Output::Preview) } Err(e) => Err(e), }}; let snapshot = Snapshot::new(dir); let reset_selection = !matches!( action.as_str(), "preview" | "pdf-page" | "details" | "download" | "more" | "notifications" | "clear-notifications" ); let _ = weak.upgrade_in_event_loop(move |app| { if generation.load(std::sync::atomic::Ordering::SeqCst) != current_generation { return; } let ui = app.global::(); if app.get_active_view() != "files" { ui.set_busy(false); ui.set_list_loading(false); preview_pending.store(false, std::sync::atomic::Ordering::SeqCst); return; } snapshot.apply(&ui); if action == "connect" && result.is_ok() { ui.set_connection_revision(ui.get_connection_revision().wrapping_add(1)); } if action == "more" || result.is_ok() { ui.set_pagination_failed(action == "more" && result.is_err()); } if result.is_ok() && !matches!(action.as_str(), "preview" | "pdf-page" | "details" | "download" | "notifications" | "clear-notifications" | "transfers") { ui.set_content_revision(ui.get_content_revision().wrapping_add(1)); } if reset_selection { ui.set_selected(-1); } match result { Ok(Output::Operations(rows)) => { ui.set_operations(ModelRc::new(VecModel::from( rows.into_iter() .map(|r| OperationRow { id: r.id.to_string().into(), action: format!("#{} · {} · {}",r.id,r.action,r.description).into(), state: r.state.into(), error: r.error.into(), bytes: if r.bytes>0 {size(r.bytes).into()}else{slint::SharedString::default()}, }) .collect::>(), ))); ui.set_transfers_open(true); } Ok(Output::Preview(preview)) => { ui.set_activity_open(matches!( action.as_str(), "notifications" | "clear-notifications" )); match preview { Preview::Text(text) => { ui.set_preview_text(text.into()); ui.set_preview_is_image(false); } Preview::Pdf(data, page) => { *pdf_document.lock().unwrap() = Some(data); ui.set_pdf_page(page.index as i32); ui.set_pdf_pages(page.count as i32); let buffer = SharedPixelBuffer::::clone_from_slice(&page.pixels, page.width, page.height); ui.set_preview_image(slint::Image::from_rgba8(buffer)); ui.set_preview_is_image(true); } Preview::Image(data, w, h) => { let buffer = SharedPixelBuffer::::clone_from_slice(&data, w, h); ui.set_preview_image(slint::Image::from_rgba8(buffer)); ui.set_preview_is_image(true); } } ui.set_preview_open(true); } Ok(Output::Status(message)) => ui.set_status(message.into()), Err(error) => ui.set_status(error.into()), } preview_pending.store(false, std::sync::atomic::Ordering::SeqCst); ui.set_list_loading(false); ui.set_busy(false); }); }); *pending.borrow_mut() = Some(task.abort_handle()); }); } enum Output { Operations(Vec), Status(String), Preview(Preview), } fn reset_profile(ui: &FilesUi) { ui.set_rows(ModelRc::new(VecModel::from(Vec::::new()))); ui.set_operations(ModelRc::new(VecModel::from(Vec::::new()))); ui.set_accounts(ModelRc::new(VecModel::from(vec![ slint::SharedString::from("All accounts"), ]))); ui.set_storage_accounts(ModelRc::new(VecModel::from( Vec::::new(), ))); ui.set_account(0); ui.set_selected_account_id(-1); ui.set_storage_account(0); ui.set_selected(-1); ui.set_pdf_pages(0); ui.set_pdf_zoom(100); ui.set_pagination_failed(false); ui.set_busy(false); ui.set_list_loading(false); ui.set_connected(false); ui.set_can_create(false); ui.set_can_more(false); ui.set_can_up(false); ui.set_preview_open(false); ui.set_preview_text("".into()); ui.set_preview_image(slint::Image::default()); ui.set_dialog("".into()); ui.set_transfers_open(false); ui.set_attachments(true); ui.set_server_attachments(false); ui.set_query("".into()); ui.set_endpoint("".into()); ui.set_quota("".into()); ui.set_status("".into()); } fn size(bytes: u64) -> String { if bytes < 1024 { format!("{bytes} B") } else if bytes < 1024 * 1024 { format!("{:.1} KB", bytes as f64 / 1024.) } else { format!("{:.1} MB", bytes as f64 / (1024. * 1024.)) } } struct Snapshot { rows: Vec, accounts: Vec, account: i32, account_id: i32, spaces: Vec, space: i32, attachments: bool, connected: bool, endpoint: String, webdav: bool, can_create: bool, can_lock: bool, can_more: bool, can_up: bool, path: String, query: String, quota: String, } /// The local day of a file's date. Snapshots are built off the UI thread, /// which formats it with the current language in `Snapshot::apply`. fn iso_day(date: chrono::DateTime) -> String { date.format("%Y-%m-%d").to_string() } /// JMAP stores RFC 3339 times and WebDAV reports RFC 2822 ones. fn modified_day(modified: &str) -> Option { chrono::DateTime::parse_from_rfc3339(modified) .or_else(|_| chrono::DateTime::parse_from_rfc2822(modified)) .ok() .map(|date| iso_day(date.with_timezone(&chrono::Local))) } fn file_date(formats: &DateFormats, iso_date: &str) -> SharedString { chrono::NaiveDate::parse_from_str(iso_date, "%Y-%m-%d") .map(|date| formats.date(DateStyle::Medium, date).into()) .unwrap_or_default() } impl Snapshot { fn new(dir: &Directory) -> Self { let rows = dir .entries .iter() .map(|entry| match entry { Entry::Attachment(a) => FileRow { identity: match &a.remote { Some(remote) => format!( "mail:{}:{}:{}:{}", a.account_id, remote.account, remote.email, remote.blob ), None => format!("cached:{}:{}", a.account_id, a.id), } .into(), name: a.filename.clone().into(), detail: format!("{} · {}", a.sender, a.subject).into(), size: size(a.size).into(), iso_date: chrono::DateTime::from_timestamp_millis(a.date) .map(|date| iso_day(date.with_timezone(&chrono::Local))) .unwrap_or_default() .into(), attachment: true, readable: true, ..Default::default() }, Entry::Remote(n) => FileRow { identity: format!("storage:{}:{}:{}", dir.account, dir.settings.endpoint, n.id) .into(), created: n.created.clone().unwrap_or_default().into(), modified: n.modified.clone().unwrap_or_default().into(), accessed: n.accessed.clone().unwrap_or_default().into(), executable: n.executable, subscribed: n.is_subscribed.unwrap_or(true), name: n.name.clone().into(), detail: if n.is_directory() { "Folder".into() } else { n.media_type.clone().unwrap_or_default().into() }, size: n.size.map(size).unwrap_or_default().into(), iso_date: n .modified .as_deref() .and_then(modified_day) .unwrap_or_default() .into(), directory: n.is_directory(), readable: n.my_rights.may_read, writable: n.my_rights.modify(), renameable: n.my_rights.rename(), deletable: n.my_rights.delete(), shareable: n.my_rights.may_share, locked: n.locked, owns_lock: matches!(&dir.client,Some(FileClient::Dav(c)) if c.owns_lock(&n.id)), ..Default::default() }, }) .collect(); let (spaces, space) = if dir.offline { ( dir.cached_spaces .iter() .map(|(_, name, _)| name.clone()) .collect(), dir.cached_spaces .iter() .position(|(id, _, _)| Some(*id) == dir.cached_space) .unwrap_or(0) as i32, ) } else { match &dir.client { Some(FileClient::Jmap(c)) => ( c.accounts.iter().map(|a| a.name.clone()).collect(), c.accounts .iter() .position(|a| a.id == c.account_id) .unwrap_or(0) as i32, ), _ => (Vec::new(), 0), } }; Self { rows, accounts: std::iter::once("All accounts".into()) .chain(dir.accounts.iter().map(|a| a.1.clone())) .collect(), account: dir.account as i32, account_id: dir .account_id() .and_then(|id| i32::try_from(id).ok()) .unwrap_or(-1), spaces, space, attachments: dir.attachments, connected: dir.client.is_some() || dir.cached_space.is_some(), endpoint: dir.settings.endpoint.clone(), webdav: dir.settings.webdav, can_create: dir.can_create, can_lock: matches!(&dir.client,Some(FileClient::Dav(c)) if c.supports_locks), can_more: dir.next.is_some(), can_up: !dir.history.is_empty(), path: if dir.attachments { "Files from your email, in one place".into() } else { format!( "/{}", dir.history .iter() .map(|n| n.name.as_str()) .collect::>() .join(" / ") ) }, query: dir.query.clone(), quota: dir.quota.clone(), } } fn apply(mut self, ui: &FilesUi) { let formats = date_format::current(); for row in &mut self.rows { row.date = file_date(&formats, &row.iso_date); } // Reuse Mail's reconciliation so appending pages retains the viewport // and existing row delegates instead of rebuilding the entire list. let retained = ui.get_rows(); if let Some(model) = retained.as_any().downcast_ref::>() { crate::reconcile_model_rows_by( model, self.rows, |row| row.identity.clone(), PartialEq::eq, ); } else { ui.set_rows(ModelRc::new(VecModel::from(self.rows))); } ui.set_accounts(ModelRc::new(VecModel::from( self.accounts .into_iter() .map(Into::into) .collect::>(), ))); ui.set_account(self.account); ui.set_selected_account_id(self.account_id); ui.set_storage_accounts(ModelRc::new(VecModel::from( self.spaces.into_iter().map(Into::into).collect::>(), ))); ui.set_storage_account(self.space); ui.set_attachments(self.attachments); ui.set_connected(self.connected); ui.set_endpoint(self.endpoint.into()); ui.set_webdav(self.webdav); ui.set_can_create(self.can_create); ui.set_can_lock(self.can_lock); ui.set_can_more(self.can_more); ui.set_can_up(self.can_up); ui.set_path(self.path.into()); ui.set_query(self.query.into()); ui.set_quota(self.quota.into()); } } #[cfg(test)] mod tests { use super::*; use crate::date_format::HourCycle; #[test] fn file_dates_read_jmap_and_webdav_times() { let noon = chrono::DateTime::parse_from_rfc3339("2026-10-22T12:00:00Z") .unwrap() .with_timezone(&chrono::Local); let day = iso_day(noon); assert_eq!(modified_day("2026-10-22T12:00:00Z"), Some(day.clone())); assert_eq!(modified_day("Thu, 22 Oct 2026 12:00:00 GMT"), Some(day)); assert_eq!(modified_day("yesterday"), None); let formats = DateFormats::english(HourCycle::TwentyFour); assert_eq!(file_date(&formats, "2026-10-22"), "Oct 22, 2026"); assert_eq!(file_date(&formats, ""), ""); } }