Skip to content

Commit 2cd19e0

Browse files
committed
feat(config): dynamic hot reloading
At long last, config hot reloading! This introduces a config diffing system that tries to minimise the amount of reloading that needs to take place, according to the following rules: - Monitors switching from single <--> multiple bars will need a full reload of all bars - Bars with non-module configuration changes, or a change to the number of modules are fully reloaded - Changes at a module level replace just that module. This means that you can add, remove and change entire monitor configurations, individual bars, and modules and Ironbar will do its best to minimise the impact. Hot-reloading does not currently support top-level config options other than `bar` and `monitors`. Some of these are likely to never be supported but this still requires exploration. This also adds a new top-level `hot_reload` option, which takes two formats: - `hot_reload: <true/false>` - enables/disables for both config and styles. - `hot_reload.config` and `hot_reload.style` which can be used to toggle each system separately.
1 parent 032addf commit 2cd19e0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+862
-188
lines changed

docs/Configuration guide.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,9 @@ The following table lists each of the top-level bar config options:
300300
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
301301
| `icon_overrides` | `Map<string, string>` | `{}` | Map of image inputs to override names. Usually used for app IDs (or classes) to icon names, overriding the app's default icon. |
302302
| `double_click_time` | `integer` or `"gtk"` | `250` | Time in milliseconds to wait for a double-click. Set to `"gtk"` to use GTK's setting. |
303+
| `hot_reload` | `boolean` or `HotReload` | `true` | Whether to hot-reload config and style changes. Can also take object format to toggle separately. |
304+
| `hot_reload.config` | `boolean` | `true` | Whether to hot-reload config changes. |
305+
| `hot_reload.style` | `boolean` | `true` | Whether to hot-reload style changes. |
303306

304307
> [!TIP]
305308
> `monitors` is only required if you are following **2b** or **2c** (ie not the same bar across all monitors).

docs/Development guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ When the `extras` feature is enabled, it must also derive `schemars::JsonSchema`
235235
An extract of the Clock module is shown below as an example:
236236

237237
```rust
238-
#[derive(Debug, Deserialize, Clone)]
238+
#[derive(Debug, Deserialize, Clone, PartialEq)]
239239
#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))]
240240
#[serde(default)]
241241
pub struct ClockModule {

src/bar.rs

Lines changed: 151 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::config::diff::BarDiffDetails;
12
use crate::config::{BarConfig, BarPosition, MarginConfig, ModuleConfig};
23
use crate::modules::{BarModuleFactory, ModuleInfo, ModuleLocation, ModuleRef};
34
use crate::popup::Popup;
@@ -19,6 +20,7 @@ enum Inner {
1920
Loaded {
2021
module_refs: Vec<ModuleRef>,
2122
popup: Rc<Popup>,
23+
instance: Rc<Bar>,
2224
},
2325
}
2426

@@ -145,7 +147,7 @@ impl Bar {
145147
let margin = config.margin;
146148

147149
let instance = Rc::new(self.clone());
148-
let load_result = self.load_modules(instance, config, monitor);
150+
let load_result = self.load_modules(&instance, config, monitor);
149151

150152
let autohide_state = if let Some(autohide) = autohide {
151153
let hotspot_window = Window::new();
@@ -177,6 +179,7 @@ impl Bar {
177179
self.inner = Inner::Loaded {
178180
popup: load_result.popup,
179181
module_refs: load_result.module_refs,
182+
instance,
180183
};
181184

182185
self
@@ -188,6 +191,144 @@ impl Bar {
188191
self.window.destroy();
189192
}
190193

194+
pub fn name(&self) -> &str {
195+
&self.name
196+
}
197+
198+
/// The name of the output the bar is displayed on.
199+
pub fn monitor_name(&self) -> &str {
200+
&self.monitor_name
201+
}
202+
203+
pub fn popup(&self) -> Rc<Popup> {
204+
match &self.inner {
205+
Inner::New { .. } => {
206+
panic!("Attempted to get popup of uninitialized bar. This is a serious bug!")
207+
}
208+
Inner::Loaded { popup, .. } => popup.clone(),
209+
}
210+
}
211+
212+
pub fn visible(&self) -> bool {
213+
self.window.is_visible()
214+
}
215+
216+
/// Sets the window visibility status
217+
pub fn set_visible(&self, visible: bool) {
218+
self.window.set_visible(visible);
219+
}
220+
221+
pub fn set_exclusive(&self, exclusive: bool) {
222+
if exclusive {
223+
self.window.auto_exclusive_zone_enable();
224+
} else {
225+
self.window.set_exclusive_zone(0);
226+
}
227+
}
228+
229+
pub fn set_locked(&self, locked: bool) {
230+
let mut autohide_state = self.autohide_state.borrow_mut();
231+
let Some(autohide_state) = autohide_state.as_mut() else {
232+
return;
233+
};
234+
235+
if locked {
236+
autohide_state.lock_state = LockState::Locked;
237+
} else if autohide_state.lock_state == LockState::LockedPendingClose {
238+
self.window.set_visible(false);
239+
autohide_state.hotspot_window.set_visible(true);
240+
autohide_state.lock_state = LockState::Unlocked;
241+
}
242+
}
243+
244+
pub fn modules(&self) -> &[ModuleRef] {
245+
match &self.inner {
246+
Inner::New { .. } => {
247+
panic!("Attempted to get modules of uninitialized bar. This is a serious bug!")
248+
}
249+
Inner::Loaded { module_refs, .. } => module_refs,
250+
}
251+
}
252+
253+
pub fn apply_diff(&mut self, diff: BarDiffDetails, config: BarConfig, monitor: &Monitor) {
254+
let module_factory =
255+
BarModuleFactory::new(self.ironbar.clone(), self.instance(), self.popup()).into();
256+
let app = &self.window.application().expect("to exist");
257+
258+
macro_rules! info {
259+
($location:expr) => {
260+
ModuleInfo {
261+
app,
262+
bar_position: config.position,
263+
monitor,
264+
output_name: &self.monitor_name,
265+
location: $location,
266+
}
267+
};
268+
}
269+
270+
let module_refs = match &mut self.inner {
271+
Inner::New { .. } => {
272+
panic!("Attempted to get modules of uninitialized bar. This is a serious bug!")
273+
}
274+
Inner::Loaded { module_refs, .. } => module_refs,
275+
};
276+
277+
let mut load_modules = |mut modules: Vec<ModuleConfig>,
278+
diffs: Vec<usize>,
279+
container: &gtk::Box,
280+
location: ModuleLocation| {
281+
for i in diffs.into_iter().rev() {
282+
let Some(existing_module) = module_refs
283+
.iter_mut()
284+
.filter(|m| m.location == location)
285+
.nth(i)
286+
else {
287+
error!("failed to find existing module to replace");
288+
return;
289+
};
290+
291+
// avoid potential panic if all modules are disabled, but...
292+
if modules.is_empty() {
293+
return;
294+
}
295+
296+
#[allow(unreachable_code)] // ...above check doesn't satisfy rustc
297+
let module = modules.remove(i);
298+
299+
match module.create(&module_factory, container, &info!(location)) {
300+
Ok(module) => {
301+
let existing_wrapper = existing_module
302+
.root_widget
303+
.parent()
304+
.expect("root should have revealer parent");
305+
let new_wrapper = module
306+
.root_widget
307+
.parent()
308+
.expect("root should have revealer parent");
309+
310+
container.reorder_child_after(&new_wrapper, Some(&existing_wrapper));
311+
container.remove(&existing_wrapper);
312+
let _ = std::mem::replace(existing_module, module);
313+
}
314+
Err(err) => error!("{err:?}"),
315+
}
316+
}
317+
};
318+
319+
if let Some(modules) = config.start {
320+
load_modules(modules, diff.start, &self.start, ModuleLocation::Start);
321+
}
322+
323+
if let Some(modules) = config.center {
324+
load_modules(modules, diff.center, &self.center, ModuleLocation::Center);
325+
}
326+
327+
if let Some(modules) = config.end {
328+
load_modules(modules, diff.end, &self.end, ModuleLocation::End);
329+
}
330+
}
331+
191332
/// Sets up GTK layer shell for a provided application window.
192333
fn setup_layer_shell(
193334
&self,
@@ -360,7 +501,7 @@ impl Bar {
360501
/// Loads the configured modules onto a bar.
361502
fn load_modules(
362503
&self,
363-
instance: Rc<Bar>,
504+
instance: &Rc<Bar>,
364505
config: BarConfig,
365506
monitor: &Monitor,
366507
) -> BarLoadResult {
@@ -380,7 +521,7 @@ impl Bar {
380521

381522
// popup ignores module location so can bodge this for now
382523
let popup = Popup::new(
383-
&info!(ModuleLocation::Left),
524+
&info!(ModuleLocation::Start),
384525
config.popup_gap,
385526
config.popup_autohide,
386527
);
@@ -391,13 +532,13 @@ impl Bar {
391532
if let Some(modules) = config.start {
392533
self.content.set_start_widget(Some(&self.start));
393534

394-
let info = info!(ModuleLocation::Left);
535+
let info = info!(ModuleLocation::Start);
395536
refs.extend(add_modules(
396537
&self.start,
397538
modules,
398539
&info,
399540
&self.ironbar,
400-
&instance,
541+
instance,
401542
&popup,
402543
));
403544
}
@@ -411,21 +552,21 @@ impl Bar {
411552
modules,
412553
&info,
413554
&self.ironbar,
414-
&instance,
555+
instance,
415556
&popup,
416557
));
417558
}
418559

419560
if let Some(modules) = config.end {
420561
self.content.set_end_widget(Some(&self.end));
421562

422-
let info = info!(ModuleLocation::Right);
563+
let info = info!(ModuleLocation::End);
423564
refs.extend(add_modules(
424565
&self.end,
425566
modules,
426567
&info,
427568
&self.ironbar,
428-
&instance,
569+
instance,
429570
&popup,
430571
));
431572
}
@@ -451,62 +592,12 @@ impl Bar {
451592
}
452593
}
453594

454-
pub fn name(&self) -> &str {
455-
&self.name
456-
}
457-
458-
/// The name of the output the bar is displayed on.
459-
pub fn monitor_name(&self) -> &str {
460-
&self.monitor_name
461-
}
462-
463-
pub fn popup(&self) -> Rc<Popup> {
464-
match &self.inner {
465-
Inner::New { .. } => {
466-
panic!("Attempted to get popup of uninitialized bar. This is a serious bug!")
467-
}
468-
Inner::Loaded { popup, .. } => popup.clone(),
469-
}
470-
}
471-
472-
pub fn visible(&self) -> bool {
473-
self.window.is_visible()
474-
}
475-
476-
/// Sets the window visibility status
477-
pub fn set_visible(&self, visible: bool) {
478-
self.window.set_visible(visible);
479-
}
480-
481-
pub fn set_exclusive(&self, exclusive: bool) {
482-
if exclusive {
483-
self.window.auto_exclusive_zone_enable();
484-
} else {
485-
self.window.set_exclusive_zone(0);
486-
}
487-
}
488-
489-
pub fn set_locked(&self, locked: bool) {
490-
let mut autohide_state = self.autohide_state.borrow_mut();
491-
let Some(autohide_state) = autohide_state.as_mut() else {
492-
return;
493-
};
494-
495-
if locked {
496-
autohide_state.lock_state = LockState::Locked;
497-
} else if autohide_state.lock_state == LockState::LockedPendingClose {
498-
self.window.set_visible(false);
499-
autohide_state.hotspot_window.set_visible(true);
500-
autohide_state.lock_state = LockState::Unlocked;
501-
}
502-
}
503-
504-
pub fn modules(&self) -> &[ModuleRef] {
595+
fn instance(&self) -> Rc<Bar> {
505596
match &self.inner {
506597
Inner::New { .. } => {
507598
panic!("Attempted to get modules of uninitialized bar. This is a serious bug!")
508599
}
509-
Inner::Loaded { module_refs, .. } => module_refs,
600+
Inner::Loaded { instance, .. } => instance.clone(),
510601
}
511602
}
512603
}

src/clients/outputs.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ impl MonitorProxy {
7575
tx.send_expect(MonitorEvent {
7676
connector: self.connector.clone(),
7777
state: MonitorState::Disconnected,
78-
})
78+
});
7979
}
8080
InternalMonitorState::BothConnected(wl_output, gdk_output) => {
8181
info!("Monitor {} connected", self.connector);
8282
tx.send_expect(MonitorEvent {
8383
connector: self.connector.clone(),
8484
state: MonitorState::Connected(wl_output.clone(), gdk_output.clone()),
85-
})
85+
});
8686
}
8787
_ => {}
8888
}
@@ -112,7 +112,7 @@ impl Client {
112112
}
113113
}
114114

115-
pub(crate) fn start(&self, ironbar: &Rc<Ironbar>) {
115+
pub(crate) fn start(&self, ironbar: Rc<Ironbar>) {
116116
let mut rx_wl_outputs = ironbar.clients.borrow_mut().wayland().subscribe_outputs();
117117

118118
let monitors = arc_mut!(HashMap::new());
@@ -133,13 +133,13 @@ impl Client {
133133
});
134134
match event.event_type {
135135
wayland::OutputEventType::New => {
136-
entry.connect_wayland(&event.output).maybe_send(&output_tx)
136+
entry.connect_wayland(&event.output).maybe_send(&output_tx);
137137
}
138138
wayland::OutputEventType::Destroyed => {
139-
entry.disconnect().maybe_send(&output_tx)
139+
entry.disconnect().maybe_send(&output_tx);
140140
}
141141
wayland::OutputEventType::Update => {}
142-
};
142+
}
143143
}
144144
}
145145
});

src/config/common.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use tracing::trace;
1919
/// For information on the Script type, and embedding scripts in strings,
2020
/// see [here](script).
2121
/// For information on styling, please see the [styling guide](styling-guide).
22-
#[derive(Debug, Default, Deserialize, Clone)]
22+
#[derive(Debug, Default, Deserialize, Clone, PartialEq)]
2323
#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))]
2424
pub struct CommonConfig {
2525
/// Sets the unique widget name,
@@ -208,7 +208,7 @@ pub struct CommonConfig {
208208
pub disable_popup: bool,
209209
}
210210

211-
#[derive(Debug, Deserialize, Clone)]
211+
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
212212
#[serde(rename_all = "snake_case")]
213213
#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))]
214214
pub enum TransitionType {
@@ -218,7 +218,7 @@ pub enum TransitionType {
218218
SlideEnd,
219219
}
220220

221-
#[derive(Debug, Default, Deserialize, Clone, Copy)]
221+
#[derive(Debug, Default, Deserialize, Clone, Copy, PartialEq, Eq)]
222222
#[serde(rename_all = "snake_case")]
223223
#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))]
224224
pub enum ModuleOrientation {
@@ -247,7 +247,7 @@ impl From<ModuleOrientation> for Orientation {
247247
}
248248
}
249249

250-
#[derive(Debug, Default, Deserialize, Clone, Copy)]
250+
#[derive(Debug, Default, Deserialize, Clone, Copy, PartialEq, Eq)]
251251
#[serde(rename_all = "snake_case")]
252252
#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))]
253253
pub enum ModuleJustification {

0 commit comments

Comments
 (0)