From 26686739a09d23de576b9fe3d4aedc0bf695c5f4 Mon Sep 17 00:00:00 2001 From: Reinout Meliesie Date: Sun, 10 Aug 2025 16:37:55 +0200 Subject: [PATCH] WIP update of networkmanager & volume modules --- src/clients/networkmanager/dbus.rs | 46 ++-- src/clients/networkmanager/mod.rs | 334 ++++++++++++++-------------- src/clients/networkmanager/state.rs | 40 ++-- src/modules/networkmanager.rs | 104 ++++++--- src/modules/volume.rs | 321 ++++---------------------- 5 files changed, 323 insertions(+), 522 deletions(-) diff --git a/src/clients/networkmanager/dbus.rs b/src/clients/networkmanager/dbus.rs index 5ab3889..b3dd915 100644 --- a/src/clients/networkmanager/dbus.rs +++ b/src/clients/networkmanager/dbus.rs @@ -1,71 +1,71 @@ use color_eyre::Result; -use zbus::dbus_proxy; +use zbus::proxy; use zbus::zvariant::{ObjectPath, OwnedValue, Str}; -#[dbus_proxy( +#[proxy( default_service = "org.freedesktop.NetworkManager", interface = "org.freedesktop.NetworkManager", default_path = "/org/freedesktop/NetworkManager" )] -trait Dbus { - #[dbus_proxy(property)] +pub(super) trait Dbus { + #[zbus(property)] fn active_connections(&self) -> Result>; - #[dbus_proxy(property)] + #[zbus(property)] fn devices(&self) -> Result>; - // #[dbus_proxy(property)] + // #[zbus(property)] // fn networking_enabled(&self) -> Result; - // #[dbus_proxy(property)] + // #[zbus(property)] // fn primary_connection(&self) -> Result; - // #[dbus_proxy(property)] + // #[zbus(property)] // fn primary_connection_type(&self) -> Result; - // #[dbus_proxy(property)] + // #[zbus(property)] // fn wireless_enabled(&self) -> Result; } -#[dbus_proxy( +#[proxy( default_service = "org.freedesktop.NetworkManager", interface = "org.freedesktop.NetworkManager.Connection.Active" )] -trait ActiveConnectionDbus { - // #[dbus_proxy(property)] +pub(super) trait ActiveConnectionDbus { + // #[zbus(property)] // fn connection(&self) -> Result; - // #[dbus_proxy(property)] + // #[zbus(property)] // fn default(&self) -> Result; - // #[dbus_proxy(property)] + // #[zbus(property)] // fn default6(&self) -> Result; - #[dbus_proxy(property)] + #[zbus(property)] fn devices(&self) -> Result>; - // #[dbus_proxy(property)] + // #[zbus(property)] // fn id(&self) -> Result; - #[dbus_proxy(property)] + #[zbus(property)] fn type_(&self) -> Result; - // #[dbus_proxy(property)] + // #[zbus(property)] // fn uuid(&self) -> Result; } -#[dbus_proxy( +#[proxy( default_service = "org.freedesktop.NetworkManager", interface = "org.freedesktop.NetworkManager.Device" )] -trait DeviceDbus { - // #[dbus_proxy(property)] +pub(super) trait DeviceDbus { + // #[zbus(property)] // fn active_connection(&self) -> Result; - #[dbus_proxy(property)] + #[zbus(property)] fn device_type(&self) -> Result; - #[dbus_proxy(property)] + #[zbus(property)] fn state(&self) -> Result; } diff --git a/src/clients/networkmanager/mod.rs b/src/clients/networkmanager/mod.rs index 40c64e1..5f75b08 100644 --- a/src/clients/networkmanager/mod.rs +++ b/src/clients/networkmanager/mod.rs @@ -2,17 +2,16 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; use color_eyre::Result; +use futures_lite::StreamExt; use futures_signals::signal::{Mutable, MutableSignalCloned}; use tracing::error; -use zbus::blocking::Connection; +use zbus::Connection; use zbus::zvariant::ObjectPath; -use crate::clients::networkmanager::dbus::{ - ActiveConnectionDbusProxyBlocking, DbusProxyBlocking, DeviceDbusProxyBlocking, -}; +use crate::clients::networkmanager::dbus::{ActiveConnectionDbusProxy, DbusProxy, DeviceDbusProxy}; use crate::clients::networkmanager::state::{ - determine_cellular_state, determine_vpn_state, determine_wifi_state, determine_wired_state, - CellularState, State, VpnState, WifiState, WiredState, + CellularState, State, VpnState, WifiState, WiredState, determine_cellular_state, + determine_vpn_state, determine_wifi_state, determine_wired_state, }; use crate::{ read_lock, register_fallible_client, spawn_blocking, spawn_blocking_result, write_lock, @@ -29,23 +28,23 @@ pub struct Client(Arc>); #[derive(Debug)] struct ClientInner<'l> { state: Mutable, - root_object: &'l DbusProxyBlocking<'l>, - active_connections: RwLock>>, - devices: RwLock>>, + root_object: &'l DbusProxy<'l>, + active_connections: RwLock>>, + devices: RwLock>>, dbus_connection: Connection, } impl Client { - fn new() -> Result { + async fn new() -> Result { let state = Mutable::new(State { wired: WiredState::Unknown, wifi: WifiState::Unknown, cellular: CellularState::Unknown, vpn: VpnState::Unknown, }); - let dbus_connection = Connection::system()?; + let dbus_connection = Connection::system().await?; let root_object = { - let root_object = DbusProxyBlocking::new(&dbus_connection)?; + let root_object = DbusProxy::new(&dbus_connection).await?; // Workaround for the fact that zbus (unnecessarily) requires a static lifetime here Box::leak(Box::new(root_object)) }; @@ -59,158 +58,159 @@ impl Client { }))) } - fn run(&self) -> Result<()> { - macro_rules! update_state_for_device_change { - ($client:ident) => { - $client.state.set(State { - wired: determine_wired_state(&read_lock!($client.devices))?, - wifi: determine_wifi_state(&read_lock!($client.devices))?, - cellular: determine_cellular_state(&read_lock!($client.devices))?, - vpn: $client.state.get_cloned().vpn, - }); - }; + async fn run(&self) -> Result<()> { + // TODO: Reimplement DBus watching without these write-only macros + + let mut active_connections_stream = self.0.root_object.receive_active_connections_changed().await; + while let Some(change) = active_connections_stream.next().await { + } - macro_rules! initialise_path_map { - ( - $client:expr, - $path_map:ident, - $proxy_type:ident - $(, |$new_path:ident| $property_watcher:expr)* - ) => { - let new_paths = $client.root_object.$path_map()?; - let mut path_map = HashMap::new(); - for new_path in new_paths { - let new_proxy = $proxy_type::builder(&$client.dbus_connection) - .path(new_path.clone())? - .build()?; - path_map.insert(new_path.clone(), new_proxy); - $({ - let $new_path = &new_path; - $property_watcher; - })* - } - *write_lock!($client.$path_map) = path_map; - }; - } + // ActiveConnectionDbusProxy::builder(&self.0.dbus_connection) - macro_rules! spawn_path_list_watcher { - ( - $client:expr, - $property:ident, - $property_changes:ident, - $proxy_type:ident, - |$state_client:ident| $state_update:expr - $(, |$property_client:ident, $new_path:ident| $property_watcher:expr)* - ) => { - let client = $client.clone(); - spawn_blocking_result!({ - let changes = client.root_object.$property_changes(); - for _ in changes { - let mut new_path_map = HashMap::new(); - { - let new_paths = client.root_object.$property()?; - let path_map = read_lock!(client.$property); - for new_path in new_paths { - if path_map.contains_key(&new_path) { - let proxy = path_map - .get(&new_path) - .expect("Should contain the key, guarded by runtime check"); - new_path_map.insert(new_path, proxy.to_owned()); - } else { - let new_proxy = $proxy_type::builder(&client.dbus_connection) - .path(new_path.clone())? - .build()?; - new_path_map.insert(new_path.clone(), new_proxy); - $({ - let $property_client = &client; - let $new_path = &new_path; - $property_watcher; - })* - } - } - } - *write_lock!(client.$property) = new_path_map; - let $state_client = &client; - $state_update; - } - Ok(()) - }); - } - } - - macro_rules! spawn_property_watcher { - ( - $client:expr, - $path:expr, - $property_changes:ident, - $containing_list:ident, - |$inner_client:ident| $state_update:expr - ) => { - let client = $client.clone(); - let path = $path.clone(); - spawn_blocking_result!({ - let changes = read_lock!(client.$containing_list) - .get(&path) - .expect("Should contain the key upon watcher start") - .$property_changes(); - for _ in changes { - if !read_lock!(client.$containing_list).contains_key(&path) { - break; - } - let $inner_client = &client; - $state_update; - } - Ok(()) - }); - }; - } - - initialise_path_map!( - self.0, - active_connections, - ActiveConnectionDbusProxyBlocking - ); - initialise_path_map!(self.0, devices, DeviceDbusProxyBlocking, |path| { - spawn_property_watcher!(self.0, path, receive_state_changed, devices, |client| { - update_state_for_device_change!(client); - }); - }); - self.0.state.set(State { - wired: determine_wired_state(&read_lock!(self.0.devices))?, - wifi: determine_wifi_state(&read_lock!(self.0.devices))?, - cellular: determine_cellular_state(&read_lock!(self.0.devices))?, - vpn: determine_vpn_state(&read_lock!(self.0.active_connections))?, - }); - - spawn_path_list_watcher!( - self.0, - active_connections, - receive_active_connections_changed, - ActiveConnectionDbusProxyBlocking, - |client| { - client.state.set(State { - wired: client.state.get_cloned().wired, - wifi: client.state.get_cloned().wifi, - cellular: client.state.get_cloned().cellular, - vpn: determine_vpn_state(&read_lock!(client.active_connections))?, - }); - } - ); - spawn_path_list_watcher!( - self.0, - devices, - receive_devices_changed, - DeviceDbusProxyBlocking, - |client| { - update_state_for_device_change!(client); - }, - |client, path| { - spawn_property_watcher!(client, path, receive_state_changed, devices, |client| { - update_state_for_device_change!(client); - }); - } - ); + // macro_rules! update_state_for_device_change { + // ($client:ident) => { + // $client.state.set(State { + // wired: determine_wired_state(&read_lock!($client.devices)).await?, + // wifi: determine_wifi_state(&read_lock!($client.devices)).await?, + // cellular: determine_cellular_state(&read_lock!($client.devices)).await?, + // vpn: $client.state.get_cloned().vpn, + // }); + // }; + // } + // + // macro_rules! initialise_path_map { + // ( + // $client:expr, + // $path_map:ident, + // $proxy_type:ident + // $(, |$new_path:ident| $property_watcher:expr)* + // ) => { + // let new_paths = $client.root_object.$path_map().await?; + // let mut path_map = HashMap::new(); + // for new_path in new_paths { + // let new_proxy = $proxy_type::builder(&$client.dbus_connection) + // .path(new_path.clone())? + // .build().await?; + // path_map.insert(new_path.clone(), new_proxy); + // $({ + // let $new_path = &new_path; + // $property_watcher; + // })* + // } + // *write_lock!($client.$path_map) = path_map; + // }; + // } + // + // macro_rules! spawn_path_list_watcher { + // ( + // $client:expr, + // $property:ident, + // $property_changes:ident, + // $proxy_type:ident, + // |$state_client:ident| $state_update:expr + // $(, |$property_client:ident, $new_path:ident| $property_watcher:expr)* + // ) => { + // let client = $client.clone(); + // + // let changes = client.root_object.$property_changes(); + // for _ in changes { + // let mut new_path_map = HashMap::new(); + // { + // let new_paths = client.root_object.$property()?; + // let path_map = read_lock!(client.$property); + // for new_path in new_paths { + // if path_map.contains_key(&new_path) { + // let proxy = path_map + // .get(&new_path) + // .expect("Should contain the key, guarded by runtime check"); + // new_path_map.insert(new_path, proxy.to_owned()); + // } else { + // let new_proxy = $proxy_type::builder(&client.dbus_connection) + // .path(new_path.clone())? + // .build()?; + // new_path_map.insert(new_path.clone(), new_proxy); + // $({ + // let $property_client = &client; + // let $new_path = &new_path; + // $property_watcher; + // })* + // } + // } + // } + // *write_lock!(client.$property) = new_path_map; + // let $state_client = &client; + // $state_update; + // } + // } + // } + // + // macro_rules! spawn_property_watcher { + // ( + // $client:expr, + // $path:expr, + // $property_changes:ident, + // $containing_list:ident, + // |$inner_client:ident| $state_update:expr + // ) => { + // let client = $client.clone(); + // let path = $path.clone(); + // + // let changes = read_lock!(client.$containing_list) + // .get(&path) + // .expect("Should contain the key upon watcher start") + // .$property_changes().await; + // for _ in changes { + // if !read_lock!(client.$containing_list).contains_key(&path) { + // break; + // } + // let $inner_client = &client; + // $state_update; + // } + // }; + // } + // + // initialise_path_map!(self.0, active_connections, ActiveConnectionDbusProxy); + // initialise_path_map!(self.0, devices, DeviceDbusProxy, |path| { + // spawn_property_watcher!(self.0, path, receive_state_changed, devices, |client| { + // update_state_for_device_change!(client); + // }); + // }); + // self.0.state.set(State { + // wired: determine_wired_state(&read_lock!(self.0.devices))?, + // wifi: determine_wifi_state(&read_lock!(self.0.devices))?, + // cellular: determine_cellular_state(&read_lock!(self.0.devices))?, + // vpn: determine_vpn_state(&read_lock!(self.0.active_connections))?, + // }); + // + // spawn_path_list_watcher!( + // self.0, + // active_connections, + // receive_active_connections_changed, + // ActiveConnectionDbusProxy, + // |client| { + // client.state.set(State { + // wired: client.state.get_cloned().wired, + // wifi: client.state.get_cloned().wifi, + // cellular: client.state.get_cloned().cellular, + // vpn: determine_vpn_state(&read_lock!(client.active_connections))?, + // }); + // } + // ); + // spawn_path_list_watcher!( + // self.0, + // devices, + // receive_devices_changed, + // DeviceDbusProxy, + // |client| { + // update_state_for_device_change!(client); + // }, + // |client, path| { + // spawn_property_watcher!(client, path, receive_state_changed, devices, |client| { + // update_state_for_device_change!(client); + // }); + // } + // ); Ok(()) } @@ -220,15 +220,9 @@ impl Client { } } -pub fn create_client() -> Result> { - let client = Arc::new(Client::new()?); - { - let client = client.clone(); - spawn_blocking_result!({ - client.run()?; - Ok(()) - }); - } +pub async fn create_client() -> Result> { + let client = Arc::new(Client::new().await?); + client.run().await?; Ok(client) } diff --git a/src/clients/networkmanager/state.rs b/src/clients/networkmanager/state.rs index e5e226e..079bbfa 100644 --- a/src/clients/networkmanager/state.rs +++ b/src/clients/networkmanager/state.rs @@ -1,9 +1,9 @@ use color_eyre::Result; -use crate::clients::networkmanager::dbus::{ - ActiveConnectionDbusProxyBlocking, DeviceDbusProxyBlocking, DeviceState, DeviceType, -}; use crate::clients::networkmanager::PathMap; +use crate::clients::networkmanager::dbus::{ + ActiveConnectionDbusProxy, DeviceDbusProxy, DeviceState, DeviceType, +}; #[derive(Clone, Debug)] pub struct State { @@ -56,16 +56,16 @@ pub struct VpnConnectedState { pub name: String, } -pub(super) fn determine_wired_state( - devices: &PathMap, +pub(super) async fn determine_wired_state( + devices: &PathMap<'_, DeviceDbusProxy<'_>>, ) -> Result { let mut present = false; let mut connected = false; for device in devices.values() { - if device.device_type()? == DeviceType::Ethernet { + if device.device_type().await? == DeviceType::Ethernet { present = true; - if device.state()?.is_enabled() { + if device.state().await?.is_enabled() { connected = true; break; } @@ -81,19 +81,19 @@ pub(super) fn determine_wired_state( } } -pub(super) fn determine_wifi_state( - devices: &PathMap, +pub(super) async fn determine_wifi_state( + devices: &PathMap<'_, DeviceDbusProxy<'_>>, ) -> Result { let mut present = false; let mut enabled = false; let mut connected = false; for device in devices.values() { - if device.device_type()? == DeviceType::Wifi { + if device.device_type().await? == DeviceType::Wifi { present = true; - if device.state()?.is_enabled() { + if device.state().await?.is_enabled() { enabled = true; - if device.state()? == DeviceState::Activated { + if device.state().await? == DeviceState::Activated { connected = true; break; } @@ -115,19 +115,19 @@ pub(super) fn determine_wifi_state( } } -pub(super) fn determine_cellular_state( - devices: &PathMap, +pub(super) async fn determine_cellular_state( + devices: &PathMap<'_, DeviceDbusProxy<'_>>, ) -> Result { let mut present = false; let mut enabled = false; let mut connected = false; for device in devices.values() { - if device.device_type()? == DeviceType::Modem { + if device.device_type().await? == DeviceType::Modem { present = true; - if device.state()?.is_enabled() { + if device.state().await?.is_enabled() { enabled = true; - if device.state()? == DeviceState::Activated { + if device.state().await? == DeviceState::Activated { connected = true; break; } @@ -146,11 +146,11 @@ pub(super) fn determine_cellular_state( } } -pub(super) fn determine_vpn_state( - active_connections: &PathMap, +pub(super) async fn determine_vpn_state( + active_connections: &PathMap<'_, ActiveConnectionDbusProxy<'_>>, ) -> Result { for connection in active_connections.values() { - match connection.type_()?.as_str() { + match connection.type_().await?.as_str() { "vpn" | "wireguard" => { return Ok(VpnState::Connected(VpnConnectedState { name: "unknown".into(), diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index a30be1d..5914f26 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -6,15 +6,15 @@ use gtk::{Box as GtkBox, Image, Orientation}; use serde::Deserialize; use tokio::sync::mpsc::Receiver; +use crate::channels::{AsyncSenderExt, BroadcastReceiverExt}; +use crate::clients::networkmanager::Client; use crate::clients::networkmanager::state::{ CellularState, State, VpnState, WifiState, WiredState, }; -use crate::clients::networkmanager::Client; use crate::config::CommonConfig; use crate::gtk_helpers::IronbarGtkExt; -use crate::image::ImageProvider; use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; -use crate::{glib_recv, module_impl, send_async, spawn}; +use crate::{module_impl, spawn}; #[derive(Debug, Deserialize, Clone)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] @@ -38,9 +38,9 @@ impl Module for NetworkManagerModule { fn spawn_controller( &self, - _: &ModuleInfo, + _info: &ModuleInfo, context: &WidgetContext, - _: Receiver<()>, + _rx: Receiver<()>, ) -> Result<()> { let client = context.try_client::()?; let mut client_signal = client.subscribe().to_stream(); @@ -48,7 +48,9 @@ impl Module for NetworkManagerModule { spawn(async move { while let Some(state) = client_signal.next().await { - send_async!(widget_transmitter, ModuleUpdateEvent::Update(state)); + widget_transmitter + .send_expect(ModuleUpdateEvent::Update(state)) + .await; } }); @@ -58,7 +60,7 @@ impl Module for NetworkManagerModule { fn into_widget( self, context: WidgetContext, - info: &ModuleInfo, + _info: &ModuleInfo, ) -> Result> { let container = GtkBox::new(Orientation::Horizontal, 0); @@ -86,48 +88,80 @@ impl Module for NetworkManagerModule { vpn_icon.add_class("vpn-icon"); container.add(&vpn_icon); - let icon_theme = info.icon_theme.clone(); - glib_recv!(context.subscribe(), state => { - macro_rules! update_icon { - ( - $icon_var:expr, - $state_type:ident, - {$($state:pat => $icon_name:expr,)+} - ) => { - let icon_name = match state.$state_type { - $($state => $icon_name,)+ - }; - if icon_name.is_empty() { - $icon_var.hide(); - } else { - ImageProvider::parse(icon_name, &icon_theme, false, self.icon_size) - .map(|provider| provider.load_into_image($icon_var.clone())); - $icon_var.show(); - } - }; - } + context.subscribe().recv_glib_async((), move |(), state| { + // TODO: Make this whole section less boneheaded - update_icon!(wired_icon, wired, { + let wired_icon_name = match state.wired { WiredState::Connected => "icon:network-wired-symbolic", WiredState::Disconnected => "icon:network-wired-disconnected-symbolic", WiredState::NotPresent | WiredState::Unknown => "", - }); - update_icon!(wifi_icon, wifi, { + }; + let wifi_icon_name = match state.wifi { WifiState::Connected(_) => "icon:network-wireless-connected-symbolic", WifiState::Disconnected => "icon:network-wireless-offline-symbolic", WifiState::Disabled => "icon:network-wireless-hardware-disabled-symbolic", WifiState::NotPresent | WifiState::Unknown => "", - }); - update_icon!(cellular_icon, cellular, { + }; + let cellular_icon_name = match state.cellular { CellularState::Connected => "icon:network-cellular-connected-symbolic", CellularState::Disconnected => "icon:network-cellular-offline-symbolic", CellularState::Disabled => "icon:network-cellular-hardware-disabled-symbolic", CellularState::NotPresent | CellularState::Unknown => "", - }); - update_icon!(vpn_icon, vpn, { + }; + let vpn_icon_name = match state.vpn { VpnState::Connected(_) => "icon:network-vpn-symbolic", VpnState::Disconnected | VpnState::Unknown => "", - }); + }; + + let wired_icon = wired_icon.clone(); + let wifi_icon = wifi_icon.clone(); + let cellular_icon = cellular_icon.clone(); + let vpn_icon = vpn_icon.clone(); + + let image_provider = context.ironbar.image_provider(); + + async move { + if wired_icon_name.is_empty() { + wired_icon.hide(); + } else { + image_provider + .load_into_image_silent(wired_icon_name, self.icon_size, false, &wired_icon) + .await; + wired_icon.show(); + } + + if wifi_icon_name.is_empty() { + wifi_icon.hide(); + } else { + image_provider + .load_into_image_silent(wifi_icon_name, self.icon_size, false, &wifi_icon) + .await; + wifi_icon.show(); + } + + if cellular_icon_name.is_empty() { + cellular_icon.hide(); + } else { + image_provider + .load_into_image_silent( + cellular_icon_name, + self.icon_size, + false, + &cellular_icon, + ) + .await; + cellular_icon.show(); + } + + if vpn_icon_name.is_empty() { + vpn_icon.hide(); + } else { + image_provider + .load_into_image_silent(vpn_icon_name, self.icon_size, false, &vpn_icon) + .await; + vpn_icon.show(); + } + } }); Ok(ModuleParts::new(container, None)) diff --git a/src/modules/volume.rs b/src/modules/volume.rs index 54c536e..121053c 100644 --- a/src/modules/volume.rs +++ b/src/modules/volume.rs @@ -1,21 +1,16 @@ +use crate::channels::{AsyncSenderExt, BroadcastReceiverExt}; use crate::clients::volume::{self, Event}; -use crate::config::CommonConfig; +use crate::config::{CommonConfig, LayoutConfig, TruncateMode}; use crate::gtk_helpers::IronbarGtkExt; -use crate::image::ImageProvider; use crate::modules::{ - Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext, + Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, PopupButton, WidgetContext, }; -use crate::{glib_recv, lock, module_impl, send_async, spawn, try_send}; -use glib::Propagation; -use gtk::pango::EllipsizeMode; +use crate::{lock, module_impl, spawn}; use gtk::prelude::*; -use gtk::{ - Box as GtkBox, Button, CellRendererText, ComboBoxText, Image, Label, Orientation, Scale, - ToggleButton, -}; +use gtk::{Button, Image, Label, Scale, ToggleButton}; use serde::Deserialize; -use std::collections::HashMap; use tokio::sync::mpsc; +use tracing::trace; #[derive(Debug, Clone, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] @@ -30,6 +25,16 @@ pub struct VolumeModule { #[serde(default = "default_icon_size")] icon_size: i32, + // -- Common -- + /// See [truncate options](module-level-options#truncate-mode). + /// + /// **Default**: `null` + pub(crate) truncate: Option, + + /// See [layout options](module-level-options#layout) + #[serde(default, flatten)] + layout: LayoutConfig, + /// See [common options](module-level-options#common-options). #[serde(flatten)] pub common: Option, @@ -83,26 +88,28 @@ impl Module