Add native Settings and complete live configuration reload

Add compact branded Settings with safe atomic edits, external config refresh and a draggable sidebar. Reconcile services, update connected displays and input devices, apply live layout policy and window rules, and resolve initial floating placement before tiled insertion. Package the Settings launcher and shared Ferese icon.
This commit is contained in:
Samuel Onoja committed 2026-09-26 20:31:05 +01:00
1 parent 672b4e2b60
commit 23991d5bd8
27 files changed
+3083 -61

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
//! Watch the parent directory so atomic editor saves keep working.
use crate::{Message, store::Snapshot};
use cosmic::iced::futures::{SinkExt, Stream};
use notify::{RecursiveMode, Watcher};
use std::{path::PathBuf, time::Duration};
pub fn changes(path: &PathBuf) -> impl Stream<Item = Message> + use<> {
let path = path.clone();
cosmic::iced::stream::channel(1, async move |mut output| {
let (send, mut events) = tokio::sync::mpsc::channel(1);
let target = path.clone();
let watcher = notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
if let Ok(event) = event
&& !event.kind.is_access()
&& event.paths.iter().any(|p| p == &target)
{
let _ = send.try_send(());
}
});
let mut watcher = match watcher {
Ok(watcher) => watcher,
Err(error) => {
let _ = output
.send(Message::ExternalConfig(Err(error.to_string())))
.await;
return;
}
};
if let Err(error) = watcher.watch(
path.parent().unwrap_or(std::path::Path::new(".")),
RecursiveMode::NonRecursive,
) {
let _ = output
.send(Message::ExternalConfig(Err(error.to_string())))
.await;
return;
}
// Read once after installing the watch to cover the startup race.
loop {
let file = path.clone();
let snapshot = tokio::task::spawn_blocking(move || Snapshot::read(&file))
.await
.unwrap_or_else(|e| Err(e.to_string()));
if output
.send(Message::ExternalConfig(snapshot))
.await
.is_err()
{
return;
}
if events.recv().await.is_none() {
return;
}
while tokio::time::timeout(Duration::from_millis(150), events.recv())
.await
.is_ok()
{}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use cosmic::iced::futures::{StreamExt, pin_mut};
#[test]
fn watches_atomic_replacement_and_retains_subscription_after_bad_toml() {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("config.toml");
std::fs::write(&path, "[animations]\nspeed = 1").unwrap();
let stream = changes(&path);
pin_mut!(stream);
assert!(matches!(
stream.next().await,
Some(Message::ExternalConfig(Ok(_)))
));
for source in ["bad [", "[animations]\nspeed = 0.75"] {
let temporary = directory.path().join("replacement");
std::fs::write(&temporary, source).unwrap();
std::fs::rename(temporary, &path).unwrap();
let message = tokio::time::timeout(Duration::from_secs(3), stream.next())
.await
.unwrap()
.unwrap();
match message {
Message::ExternalConfig(Ok(snapshot)) => {
assert_eq!(snapshot.source, source)
}
Message::ExternalConfig(Err(_)) => assert_eq!(source, "bad ["),
_ => panic!("unexpected watcher message"),
}
}
});
}
}