From c2ddc9768573d3ce53fd9e37efa007d607f3a372 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 19 May 2023 17:30:54 -0400 Subject: [PATCH] feat (cosmic-config): iced subscription --- cosmic-config-derive/src/lib.rs | 4 +- cosmic-config/Cargo.toml | 5 +- cosmic-config/src/lib.rs | 122 +++++++++++++++++++++++++++++++- 3 files changed, 126 insertions(+), 5 deletions(-) diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index 9f6fe37e..5f9c25e0 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -15,7 +15,7 @@ pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let name = &ast.ident; - // Get the fields of the struct + // Get the fields of the struct let fields = match ast.data { syn::Data::Struct(ref data_struct) => match data_struct.fields { syn::Fields::Named(ref fields) => &fields.named, @@ -54,7 +54,7 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let mut errors = Vec::new(); #(#get_each_config_field)* - + if errors.is_empty() { Ok(default) } else { diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index d7af68ed..5bf94df1 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -4,8 +4,9 @@ version = "0.1.0" edition = "2021" [features] -default = ["macro"] +default = ["macro", "subscription"] macro = ["cosmic-config-derive"] +subscription = ["iced", "iced_futures"] [dependencies] atomicwrites = "0.4.0" @@ -15,4 +16,6 @@ notify = "6.0.0" ron = "0.8.0" serde = "1.0.152" cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true } +iced = { path = "../iced/", optional = true } +iced_futures = { path = "../iced/futures/", optional = true } diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index b4964028..d45f18f5 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,7 +1,11 @@ -use notify::Watcher; +use iced::subscription; +use iced_futures::futures::channel::mpsc; +use notify::{RecommendedWatcher, Watcher}; use serde::{de::DeserializeOwned, Serialize}; use std::{ + borrow::Cow, fs, + hash::Hash, io::Write, path::{Path, PathBuf}, sync::Mutex, @@ -255,8 +259,122 @@ impl<'a> ConfigSet for ConfigTransaction<'a> { } } -pub trait CosmicConfigEntry where Self: Sized + Default { +#[cfg(feature = "iced")] +pub enum ConfigState { + Init(Cow<'static, str>, u64), + Waiting(T, RecommendedWatcher, mpsc::Receiver<()>, Config), + Failed, +} + +#[cfg(feature = "iced")] +pub enum ConfigUpdate { + Update(T), + UpdateError(T, Vec), + Failed, +} + +pub trait CosmicConfigEntry +where + Self: Sized + Default, +{ fn write_entry(&self, config: &Config) -> Result<(), crate::Error>; fn get_entry(config: &Config) -> Result, Self)>; } +#[cfg(feature = "iced")] +pub fn config_subscription< + I: 'static + Copy + Send + Sync + Hash, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + config_id: Cow<'static, str>, + config_version: u64, +) -> iced::Subscription<(I, Result, T)>)> { + subscription::unfold( + id, + ConfigState::Init(config_id, config_version), + move |state| start_listening_loop(id, state), + ) +} + +#[cfg(feature = "iced")] +async fn start_listening< + I: Copy, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + state: ConfigState, +) -> ( + Option<(I, Result, T)>)>, + ConfigState, +) { + use iced::futures::StreamExt; + + match state { + ConfigState::Init(config_id, version) => { + let (tx, rx) = mpsc::channel(100); + let config = match Config::new(&config_id, version) { + Ok(c) => c, + Err(e) => return (None, ConfigState::Failed), + }; + let watcher = match config.watch(move |_helper, _keys| { + let mut tx = tx.clone(); + let _ = tx.try_send(()); + }) { + Ok(w) => w, + Err(e) => return (None, ConfigState::Failed), + }; + + match T::get_entry(&config) { + Ok(t) => ( + Some((id, Ok(t.clone()))), + ConfigState::Waiting(t, watcher, rx, config), + ), + Err((errors, t)) => ( + Some((id, Err((errors, t.clone())))), + ConfigState::Waiting(t, watcher, rx, config), + ), + } + } + ConfigState::Waiting(old, watcher, mut rx, config) => match rx.next().await { + Some(_) => match T::get_entry(&config) { + Ok(t) => ( + if t != old { + Some((id, Ok(t.clone()))) + } else { + None + }, + ConfigState::Waiting(t, watcher, rx, config), + ), + Err((errors, t)) => ( + if t != old { + Some((id, Ok(t.clone()))) + } else { + None + }, + ConfigState::Waiting(t, watcher, rx, config), + ), + }, + + None => (None, ConfigState::Failed), + }, + ConfigState::Failed => iced::futures::future::pending().await, + } +} + +#[cfg(feature = "iced")] +async fn start_listening_loop< + I: Copy, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + mut state: ConfigState, +) -> ((I, Result, T)>), ConfigState) { + loop { + let (update, new_state) = start_listening(id, state).await; + state = new_state; + if let Some(update) = update { + return (update, state); + } + } +}