0.6.0 #1

Merged
pogmommy merged 89 commits from 0.6.0 into main 2025-07-04 20:29:26 -07:00
4 changed files with 87 additions and 27 deletions
Showing only changes of commit 0e409d4a52 - Show all commits

View file

@ -20,6 +20,7 @@ All notable changes to eww will be listed here, starting at changes since versio
- Fix wayland monitor names support (By: dragonnn) - Fix wayland monitor names support (By: dragonnn)
- Load systray items that are registered without a path (By: Kage-Yami) - Load systray items that are registered without a path (By: Kage-Yami)
- `get_locale` now follows POSIX standard for locale selection (By: mirhahn, w-lfchen) - `get_locale` now follows POSIX standard for locale selection (By: mirhahn, w-lfchen)
- Improve multi-monitor handling under wayland (By: bkueng)
### Features ### Features
- Add warning and docs for incompatible `:anchor` and `:exclusive` options - Add warning and docs for incompatible `:anchor` and `:exclusive` options

View file

@ -68,6 +68,7 @@ pub enum DaemonCommand {
}, },
CloseWindows { CloseWindows {
windows: Vec<String>, windows: Vec<String>,
auto_reopen: bool,
sender: DaemonResponseSender, sender: DaemonResponseSender,
}, },
KillServer, KillServer,
@ -147,16 +148,34 @@ impl<B: DisplayBackend> std::fmt::Debug for App<B> {
} }
} }
/// Wait until the .model() is available for all monitors (or there is a timeout)
async fn wait_for_monitor_model() {
let display = gdk::Display::default().expect("could not get default display");
let start = std::time::Instant::now();
loop {
let all_monitors_set =
(0..display.n_monitors()).all(|i| display.monitor(i).and_then(|monitor| monitor.model()).is_some());
if all_monitors_set {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
if std::time::Instant::now() - start > Duration::from_millis(500) {
log::warn!("Timed out waiting for monitor model to be set");
break;
}
}
}
impl<B: DisplayBackend> App<B> { impl<B: DisplayBackend> App<B> {
/// Handle a [`DaemonCommand`] event, logging any errors that occur. /// Handle a [`DaemonCommand`] event, logging any errors that occur.
pub fn handle_command(&mut self, event: DaemonCommand) { pub async fn handle_command(&mut self, event: DaemonCommand) {
if let Err(err) = self.try_handle_command(event) { if let Err(err) = self.try_handle_command(event).await {
error_handling_ctx::print_error(err); error_handling_ctx::print_error(err);
} }
} }
/// Try to handle a [`DaemonCommand`] event. /// Try to handle a [`DaemonCommand`] event.
fn try_handle_command(&mut self, event: DaemonCommand) -> Result<()> { async fn try_handle_command(&mut self, event: DaemonCommand) -> Result<()> {
log::debug!("Handling event: {:?}", &event); log::debug!("Handling event: {:?}", &event);
match event { match event {
DaemonCommand::NoOp => {} DaemonCommand::NoOp => {}
@ -174,6 +193,10 @@ impl<B: DisplayBackend> App<B> {
} }
} }
DaemonCommand::ReloadConfigAndCss(sender) => { DaemonCommand::ReloadConfigAndCss(sender) => {
// Wait for all monitor models to be set. When a new monitor gets added, this
// might not immediately be the case. And if we were to wait inside the
// connect_monitor_added callback, model() never gets set. So instead we wait here.
wait_for_monitor_model().await;
let mut errors = Vec::new(); let mut errors = Vec::new();
let config_result = config::read_from_eww_paths(&self.paths); let config_result = config::read_from_eww_paths(&self.paths);
@ -200,7 +223,7 @@ impl<B: DisplayBackend> App<B> {
DaemonCommand::CloseAll => { DaemonCommand::CloseAll => {
log::info!("Received close command, closing all windows"); log::info!("Received close command, closing all windows");
for window_name in self.open_windows.keys().cloned().collect::<Vec<String>>() { for window_name in self.open_windows.keys().cloned().collect::<Vec<String>>() {
self.close_window(&window_name)?; self.close_window(&window_name, false)?;
} }
} }
DaemonCommand::OpenMany { windows, args, should_toggle, sender } => { DaemonCommand::OpenMany { windows, args, should_toggle, sender } => {
@ -209,7 +232,7 @@ impl<B: DisplayBackend> App<B> {
.map(|w| { .map(|w| {
let (config_name, id) = w; let (config_name, id) = w;
if should_toggle && self.open_windows.contains_key(id) { if should_toggle && self.open_windows.contains_key(id) {
self.close_window(id) self.close_window(id, false)
} else { } else {
log::debug!("Config: {}, id: {}", config_name, id); log::debug!("Config: {}, id: {}", config_name, id);
let window_args = args let window_args = args
@ -240,7 +263,7 @@ impl<B: DisplayBackend> App<B> {
let is_open = self.open_windows.contains_key(&instance_id); let is_open = self.open_windows.contains_key(&instance_id);
let result = if should_toggle && is_open { let result = if should_toggle && is_open {
self.close_window(&instance_id) self.close_window(&instance_id, false)
} else { } else {
self.open_window(&WindowArguments { self.open_window(&WindowArguments {
instance_id, instance_id,
@ -256,9 +279,10 @@ impl<B: DisplayBackend> App<B> {
sender.respond_with_result(result)?; sender.respond_with_result(result)?;
} }
DaemonCommand::CloseWindows { windows, sender } => { DaemonCommand::CloseWindows { windows, auto_reopen, sender } => {
let errors = windows.iter().map(|window| self.close_window(window)).filter_map(Result::err); let errors = windows.iter().map(|window| self.close_window(window, auto_reopen)).filter_map(Result::err);
sender.respond_with_error_list(errors)?; // Ignore sending errors, as the channel might already be closed
let _ = sender.respond_with_error_list(errors);
} }
DaemonCommand::PrintState { all, sender } => { DaemonCommand::PrintState { all, sender } => {
let scope_graph = self.scope_graph.borrow(); let scope_graph = self.scope_graph.borrow();
@ -360,7 +384,7 @@ impl<B: DisplayBackend> App<B> {
} }
/// Close a window and do all the required cleanups in the scope_graph and script_var_handler /// Close a window and do all the required cleanups in the scope_graph and script_var_handler
fn close_window(&mut self, instance_id: &str) -> Result<()> { fn close_window(&mut self, instance_id: &str, auto_reopen: bool) -> Result<()> {
if let Some(old_abort_send) = self.window_close_timer_abort_senders.remove(instance_id) { if let Some(old_abort_send) = self.window_close_timer_abort_senders.remove(instance_id) {
_ = old_abort_send.send(()); _ = old_abort_send.send(());
} }
@ -380,7 +404,17 @@ impl<B: DisplayBackend> App<B> {
self.script_var_handler.stop_for_variable(unused_var.clone()); self.script_var_handler.stop_for_variable(unused_var.clone());
} }
if auto_reopen {
self.failed_windows.insert(instance_id.to_string());
// There might be an alternative monitor available already, so try to re-open it immediately.
// This can happen for example when a monitor gets disconnected and another connected,
// and the connection event happens before the disconnect.
if let Some(window_arguments) = self.instance_id_to_args.get(instance_id) {
let _ = self.open_window(&window_arguments.clone());
}
} else {
self.instance_id_to_args.remove(instance_id); self.instance_id_to_args.remove(instance_id);
}
Ok(()) Ok(())
} }
@ -392,7 +426,7 @@ impl<B: DisplayBackend> App<B> {
// if an instance of this is already running, close it // if an instance of this is already running, close it
if self.open_windows.contains_key(instance_id) { if self.open_windows.contains_key(instance_id) {
self.close_window(instance_id)?; self.close_window(instance_id, false)?;
} }
self.instance_id_to_args.insert(instance_id.to_string(), window_args.clone()); self.instance_id_to_args.insert(instance_id.to_string(), window_args.clone());
@ -445,9 +479,17 @@ impl<B: DisplayBackend> App<B> {
move |_| { move |_| {
// we don't care about the actual error response from the daemon as this is mostly just a fallback. // we don't care about the actual error response from the daemon as this is mostly just a fallback.
// Generally, this should get disconnected before the gtk window gets destroyed. // Generally, this should get disconnected before the gtk window gets destroyed.
// It serves as a fallback for when the window is closed manually. // This callback is triggered in 2 cases:
// - When the monitor of this window gets disconnected
// - When the window is closed manually.
// We don't distinguish here and assume the window should be reopened once a monitor
// becomes available again
let (response_sender, _) = daemon_response::create_pair(); let (response_sender, _) = daemon_response::create_pair();
let command = DaemonCommand::CloseWindows { windows: vec![instance_id.clone()], sender: response_sender }; let command = DaemonCommand::CloseWindows {
windows: vec![instance_id.clone()],
auto_reopen: true,
sender: response_sender,
};
if let Err(err) = app_evt_sender.send(command) { if let Err(err) = app_evt_sender.send(command) {
log::error!("Error sending close window command to daemon after gtk window destroy event: {}", err); log::error!("Error sending close window command to daemon after gtk window destroy event: {}", err);
} }
@ -466,7 +508,7 @@ impl<B: DisplayBackend> App<B> {
tokio::select! { tokio::select! {
_ = glib::timeout_future(duration) => { _ = glib::timeout_future(duration) => {
let (response_sender, mut response_recv) = daemon_response::create_pair(); let (response_sender, mut response_recv) = daemon_response::create_pair();
let command = DaemonCommand::CloseWindows { windows: vec![instance_id.clone()], sender: response_sender }; let command = DaemonCommand::CloseWindows { windows: vec![instance_id.clone()], auto_reopen: false, sender: response_sender };
if let Err(err) = app_evt_sender.send(command) { if let Err(err) = app_evt_sender.send(command) {
log::error!("Error sending close window command to daemon after gtk window destroy event: {}", err); log::error!("Error sending close window command to daemon after gtk window destroy event: {}", err);
} }

View file

@ -292,7 +292,7 @@ impl ActionWithServer {
}) })
} }
ActionWithServer::CloseWindows { windows } => { ActionWithServer::CloseWindows { windows } => {
return with_response_channel(|sender| app::DaemonCommand::CloseWindows { windows, sender }); return with_response_channel(|sender| app::DaemonCommand::CloseWindows { windows, auto_reopen: false, sender });
} }
ActionWithServer::Reload => return with_response_channel(app::DaemonCommand::ReloadConfigAndCss), ActionWithServer::Reload => return with_response_channel(app::DaemonCommand::ReloadConfigAndCss),
ActionWithServer::ListWindows => return with_response_channel(app::DaemonCommand::ListWindows), ActionWithServer::ListWindows => return with_response_channel(app::DaemonCommand::ListWindows),

View file

@ -105,13 +105,15 @@ pub fn initialize_server<B: DisplayBackend>(
} }
} }
connect_monitor_added(ui_send.clone());
// initialize all the handlers and tasks running asyncronously // initialize all the handlers and tasks running asyncronously
let tokio_handle = init_async_part(app.paths.clone(), ui_send); let tokio_handle = init_async_part(app.paths.clone(), ui_send);
gtk::glib::MainContext::default().spawn_local(async move { gtk::glib::MainContext::default().spawn_local(async move {
// if an action was given to the daemon initially, execute it first. // if an action was given to the daemon initially, execute it first.
if let Some(action) = action { if let Some(action) = action {
app.handle_command(action); app.handle_command(action).await;
} }
loop { loop {
@ -120,7 +122,7 @@ pub fn initialize_server<B: DisplayBackend>(
app.scope_graph.borrow_mut().handle_scope_graph_event(scope_graph_evt); app.scope_graph.borrow_mut().handle_scope_graph_event(scope_graph_evt);
}, },
Some(ui_event) = ui_recv.recv() => { Some(ui_event) = ui_recv.recv() => {
app.handle_command(ui_event); app.handle_command(ui_event).await;
} }
else => break, else => break,
} }
@ -136,6 +138,29 @@ pub fn initialize_server<B: DisplayBackend>(
Ok(ForkResult::Child) Ok(ForkResult::Child)
} }
fn connect_monitor_added(ui_send: UnboundedSender<DaemonCommand>) {
let display = gtk::gdk::Display::default().expect("could not get default display");
display.connect_monitor_added({
move |_display: &gtk::gdk::Display, _monitor: &gtk::gdk::Monitor| {
log::info!("New monitor connected, reloading configuration");
let _ = reload_config_and_css(&ui_send);
}
});
}
fn reload_config_and_css(ui_send: &UnboundedSender<DaemonCommand>) -> Result<()> {
let (daemon_resp_sender, mut daemon_resp_response) = daemon_response::create_pair();
ui_send.send(DaemonCommand::ReloadConfigAndCss(daemon_resp_sender))?;
tokio::spawn(async move {
match daemon_resp_response.recv().await {
Some(daemon_response::DaemonResponse::Success(_)) => log::info!("Reloaded config successfully"),
Some(daemon_response::DaemonResponse::Failure(e)) => eprintln!("{}", e),
None => log::error!("No response to reload configuration-reload request"),
}
});
Ok(())
}
fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender<app::DaemonCommand>) -> tokio::runtime::Handle { fn init_async_part(paths: EwwPaths, ui_send: UnboundedSender<app::DaemonCommand>) -> tokio::runtime::Handle {
let rt = tokio::runtime::Builder::new_multi_thread() let rt = tokio::runtime::Builder::new_multi_thread()
.thread_name("main-async-runtime") .thread_name("main-async-runtime")
@ -216,20 +241,12 @@ async fn run_filewatch<P: AsRef<Path>>(config_dir: P, evt_send: UnboundedSender<
debounce_done.store(true, Ordering::SeqCst); debounce_done.store(true, Ordering::SeqCst);
}); });
let (daemon_resp_sender, mut daemon_resp_response) = daemon_response::create_pair();
// without this sleep, reading the config file sometimes gives an empty file. // without this sleep, reading the config file sometimes gives an empty file.
// This is probably a result of editors not locking the file correctly, // This is probably a result of editors not locking the file correctly,
// and eww being too fast, thus reading the file while it's empty. // and eww being too fast, thus reading the file while it's empty.
// There should be some cleaner solution for this, but this will do for now. // There should be some cleaner solution for this, but this will do for now.
tokio::time::sleep(std::time::Duration::from_millis(50)).await; tokio::time::sleep(std::time::Duration::from_millis(50)).await;
evt_send.send(app::DaemonCommand::ReloadConfigAndCss(daemon_resp_sender))?; reload_config_and_css(&evt_send)?;
tokio::spawn(async move {
match daemon_resp_response.recv().await {
Some(daemon_response::DaemonResponse::Success(_)) => log::info!("Reloaded config successfully"),
Some(daemon_response::DaemonResponse::Failure(e)) => eprintln!("{}", e),
None => log::error!("No response to reload configuration-reload request"),
}
});
} }
}, },
else => break else => break