launcher: open the capsule on the currently-focused monitor
launcher_command::spawn() only runs for the primary App instance, and AppInput::OpenLauncher always grabbed focus on that instance's own capsule — but self.monitor is whichever output was focused ONCE, at that instance's own init(), not re-resolved on every keybind press. Start breadbar while eDP-1 is focused, move to DVI-I-1, press the launcher keybind, and the capsule opened on eDP-1, off-screen from where the user was looking. Re-resolve the focused Hyprland monitor at fire-time via primary_hypr_monitor() (already used for the same purpose at startup) and route to the right instance: the primary's own capsule if the focused monitor is its own, or forward to that monitor's tracked Controller<App> in self.satellites otherwise. Falls back to the local capsule when there's no focused monitor to resolve (screenshot mode, hyprctl unavailable) or no tracked satellite for it yet (a very recent hotplug reconcile hasn't caught up with). Split the routing decision into a pure resolve_launcher_route() helper so it's unit-testable without a live App/GTK/Hyprland stack; covered by four new tests.
This commit is contained in:
parent
110ad2c6f9
commit
2cbdb58f37
1 changed files with 93 additions and 1 deletions
92
src/main.rs
92
src/main.rs
|
|
@ -1705,11 +1705,35 @@ impl SimpleComponent for App {
|
||||||
self.panels.hide_all();
|
self.panels.hide_all();
|
||||||
}
|
}
|
||||||
AppInput::OpenLauncher => {
|
AppInput::OpenLauncher => {
|
||||||
|
// Only the primary instance subscribes to the open command
|
||||||
|
// (`launcher_command::spawn`), but `self.monitor` here is
|
||||||
|
// whichever output was focused ONCE, at this instance's own
|
||||||
|
// `init()` — baked in at process start, not re-resolved on
|
||||||
|
// every keybind press. If the user has since moved focus to
|
||||||
|
// a different monitor, blindly grabbing focus on `self`
|
||||||
|
// would open the capsule on the wrong screen. Re-resolve
|
||||||
|
// the focused monitor now and route to whichever instance
|
||||||
|
// actually owns it.
|
||||||
|
let satellite_names: Vec<&str> =
|
||||||
|
self.satellites.iter().map(|(n, _)| n.as_str()).collect();
|
||||||
|
let focused = primary_hypr_monitor();
|
||||||
|
match resolve_launcher_route(focused.as_deref(), &self.monitor, &satellite_names)
|
||||||
|
{
|
||||||
|
LauncherRoute::Satellite(name) => {
|
||||||
|
// resolve_launcher_route only returns a name present in
|
||||||
|
// satellite_names, so this lookup cannot miss.
|
||||||
|
if let Some((_, ctrl)) = self.satellites.iter().find(|(n, _)| *n == name) {
|
||||||
|
ctrl.sender().emit(AppInput::OpenLauncher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LauncherRoute::Local => {
|
||||||
self.launcher_entry.grab_focus();
|
self.launcher_entry.grab_focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The `widget:<key>` alias `for_each_in_slot` recognizes for each
|
/// The `widget:<key>` alias `for_each_in_slot` recognizes for each
|
||||||
/// `WidgetPlacement` variant — the fallback a `WidgetSpec` routes through
|
/// `WidgetPlacement` variant — the fallback a `WidgetSpec` routes through
|
||||||
|
|
@ -2847,6 +2871,35 @@ fn primary_hypr_monitor() -> Option<String> {
|
||||||
.map(|m| m.name.clone())
|
.map(|m| m.name.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where `AppInput::OpenLauncher` should be actually handled: locally (this
|
||||||
|
/// instance grabs its own capsule's focus), or forwarded to a specific
|
||||||
|
/// satellite instance. Pure decision logic, split out of the `update` match
|
||||||
|
/// arm so it's unit-testable without a live `App`/GTK/Hyprland stack.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
enum LauncherRoute {
|
||||||
|
Local,
|
||||||
|
Satellite(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `focused`: the currently-focused Hyprland monitor, re-queried at
|
||||||
|
/// keybind-fire time (`None` if Hyprland's monitor query failed, e.g.
|
||||||
|
/// screenshot mode). `own`: this instance's own monitor, fixed at `init()`.
|
||||||
|
/// `satellites`: names of monitors this (necessarily primary) instance
|
||||||
|
/// tracks a `Controller<App>` for.
|
||||||
|
///
|
||||||
|
/// Falls back to `Local` whenever forwarding isn't possible or isn't
|
||||||
|
/// needed, so a caller can always make forward progress: no focused
|
||||||
|
/// monitor, the focused monitor is this instance's own, or the focused
|
||||||
|
/// monitor has no tracked satellite yet.
|
||||||
|
fn resolve_launcher_route(focused: Option<&str>, own: &str, satellites: &[&str]) -> LauncherRoute {
|
||||||
|
match focused {
|
||||||
|
Some(name) if name != own && satellites.contains(&name) => {
|
||||||
|
LauncherRoute::Satellite(name.to_string())
|
||||||
|
}
|
||||||
|
_ => LauncherRoute::Local,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn hypr_monitor_names() -> Vec<String> {
|
fn hypr_monitor_names() -> Vec<String> {
|
||||||
hypr_monitors_live()
|
hypr_monitors_live()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -2937,3 +2990,42 @@ fn drop_satellite(satellites: &mut Vec<(String, Controller<App>)>, name: &str) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod launcher_route_tests {
|
||||||
|
use super::{resolve_launcher_route, LauncherRoute};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focused_monitor_is_own_stays_local() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_launcher_route(Some("eDP-1"), "eDP-1", &["DVI-I-1"]),
|
||||||
|
LauncherRoute::Local
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focused_monitor_is_tracked_satellite_forwards() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_launcher_route(Some("DVI-I-1"), "eDP-1", &["DVI-I-1"]),
|
||||||
|
LauncherRoute::Satellite("DVI-I-1".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focused_monitor_with_no_tracked_satellite_falls_back_local() {
|
||||||
|
// e.g. reconcile hasn't caught up with a very recent hotplug yet.
|
||||||
|
assert_eq!(
|
||||||
|
resolve_launcher_route(Some("HDMI-A-1"), "eDP-1", &["DVI-I-1"]),
|
||||||
|
LauncherRoute::Local
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_focused_monitor_falls_back_local() {
|
||||||
|
// Hyprland's monitor query failed (screenshot mode, hyprctl missing).
|
||||||
|
assert_eq!(
|
||||||
|
resolve_launcher_route(None, "eDP-1", &["DVI-I-1"]),
|
||||||
|
LauncherRoute::Local
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue