Window swap/focus/reset hotkeys; clamp layout tiles to the monitor.
Do not run live compositor tests that move the user's windows.
This commit is contained in:
parent
50b2aebee6
commit
af45b054cc
@ -58,6 +58,8 @@ enboxer doctor
|
|||||||
|
|
||||||
The GUI saves `~/.config/enboxer/profile.yaml`. Start routing from **Session → Start routing**.
|
The GUI saves `~/.config/enboxer/profile.yaml`. Start routing from **Session → Start routing**.
|
||||||
|
|
||||||
|
**Layout** places every captured client (stacked / grid / main+strip), then **Make main** swaps a minion into the large tile. Hotkeys: Ctrl+` swap next, Ctrl+F1… focus slot, Ctrl+Shift+F2… swap that slot to main, Ctrl+Shift+R reset layout.
|
||||||
|
|
||||||
`enboxer press Alt+G` fires a map without a Hyprland bind (daemon must be running).
|
`enboxer press Alt+G` fires a map without a Hyprland bind (daemon must be running).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@ -40,6 +40,15 @@ interact:
|
|||||||
|
|
||||||
session_hotkeys:
|
session_hotkeys:
|
||||||
mode_cycle: "Shift+Alt+M"
|
mode_cycle: "Shift+Alt+M"
|
||||||
|
swap_next: "Ctrl+grave"
|
||||||
|
swap_prev: "Ctrl+Shift+grave"
|
||||||
|
focus_next: "Ctrl+Shift+N"
|
||||||
|
focus_prev: "Ctrl+Shift+P"
|
||||||
|
focus_main: "Ctrl+F1"
|
||||||
|
reset_all: "Ctrl+Shift+R"
|
||||||
|
stay_on_top: "Ctrl+Shift+T"
|
||||||
|
mouse_follow: "Ctrl+Shift+F"
|
||||||
|
mouse_broadcast: "Ctrl+Shift+B"
|
||||||
|
|
||||||
characters:
|
characters:
|
||||||
- slot: 1
|
- slot: 1
|
||||||
|
|||||||
53
src/gui.rs
53
src/gui.rs
@ -6,6 +6,7 @@ use crate::profile::{
|
|||||||
default_config_path, Group, LayoutPreset, Map, Mode, NormRect, Profile, Repeater, Step,
|
default_config_path, Group, LayoutPreset, Map, Mode, NormRect, Profile, Repeater, Step,
|
||||||
VideoFx, WindowMatch,
|
VideoFx, WindowMatch,
|
||||||
};
|
};
|
||||||
|
use crate::session;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use eframe::egui::{self, Color32};
|
use eframe::egui::{self, Color32};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@ -99,6 +100,24 @@ impl App {
|
|||||||
std::env::current_exe().unwrap_or_else(|_| PathBuf::from("enboxer"))
|
std::env::current_exe().unwrap_or_else(|_| PathBuf::from("enboxer"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ipc(&mut self, verb: &str, arg: &str) {
|
||||||
|
let sock = session::default_sock();
|
||||||
|
let mut cmd = Command::new(Self::exe());
|
||||||
|
cmd.arg("ipc").arg("--sock").arg(&sock).arg(verb);
|
||||||
|
if !arg.is_empty() {
|
||||||
|
cmd.arg(arg);
|
||||||
|
}
|
||||||
|
match cmd.output() {
|
||||||
|
Ok(o) => {
|
||||||
|
self.status = String::from_utf8_lossy(&o.stdout).trim().to_string();
|
||||||
|
if !o.status.success() {
|
||||||
|
self.error = Some(String::from_utf8_lossy(&o.stderr).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => self.error = Some(format!("daemon not running? {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn save(&mut self) {
|
fn save(&mut self) {
|
||||||
if let Some(dir) = self.path.parent() {
|
if let Some(dir) = self.path.parent() {
|
||||||
let _ = std::fs::create_dir_all(dir);
|
let _ = std::fs::create_dir_all(dir);
|
||||||
@ -455,8 +474,38 @@ impl App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Refresh windows").clicked() {
|
ui.horizontal(|ui| {
|
||||||
self.refresh_status();
|
if ui.button("Capture / refresh").clicked() {
|
||||||
|
self.refresh_status();
|
||||||
|
}
|
||||||
|
if ui.button("Reset to saved layout").clicked() {
|
||||||
|
self.ipc("reset-all", "");
|
||||||
|
}
|
||||||
|
if ui.button("Swap next into main").clicked() {
|
||||||
|
self.ipc("swap-next", "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.checkbox(
|
||||||
|
&mut self.profile.layout.pin,
|
||||||
|
"Stay on top (new layout tiles)",
|
||||||
|
);
|
||||||
|
ui.label("Captured clients (Focus / Make main):");
|
||||||
|
if let Ok(out) = Command::new("hyprctl").args(["-j", "clients"]).output() {
|
||||||
|
if let Ok(clients) = serde_json::from_slice::<Vec<crate::hypr::Client>>(&out.stdout) {
|
||||||
|
let wins = crate::layout::select_windows(&self.profile, clients);
|
||||||
|
for (slot, c) in &wins {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let kind = if c.xwayland { "X11" } else { "Wayland" };
|
||||||
|
ui.label(format!("#{slot} {kind} {} {}", c.class, c.title));
|
||||||
|
if ui.small_button("Focus").clicked() {
|
||||||
|
self.ipc("focus", &slot.to_string());
|
||||||
|
}
|
||||||
|
if *slot > 1 && ui.small_button("Make main").clicked() {
|
||||||
|
self.ipc("swap", &slot.to_string());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ui.label("XWayland/Wine windows get keys while in the background. Native Wayland windows are focused briefly.");
|
ui.label("XWayland/Wine windows get keys while in the background. Native Wayland windows are focused briefly.");
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
|||||||
@ -48,7 +48,7 @@ pub fn generate(layout: &Layout, n: u32, mons: &[Monitor]) -> Vec<LayoutSlot> {
|
|||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let m = pick_monitor(mons, &layout.monitor);
|
let m = pick_monitor(mons, &layout.monitor);
|
||||||
match layout.preset {
|
let mut out = match layout.preset {
|
||||||
LayoutPreset::Stacked => {
|
LayoutPreset::Stacked => {
|
||||||
let (w, h) = constrain(m.width, m.height);
|
let (w, h) = constrain(m.width, m.height);
|
||||||
(0..n)
|
(0..n)
|
||||||
@ -63,7 +63,20 @@ pub fn generate(layout: &Layout, n: u32, mons: &[Monitor]) -> Vec<LayoutSlot> {
|
|||||||
}
|
}
|
||||||
LayoutPreset::Grid => grid(m, n, layout.pin),
|
LayoutPreset::Grid => grid(m, n, layout.pin),
|
||||||
LayoutPreset::MainStrip => main_strip(m, n, layout),
|
LayoutPreset::MainStrip => main_strip(m, n, layout),
|
||||||
|
};
|
||||||
|
for s in &mut out {
|
||||||
|
clamp_to_monitor(s, m);
|
||||||
}
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clamp_to_monitor(s: &mut LayoutSlot, m: &Monitor) {
|
||||||
|
s.w = s.w.clamp(64, m.width.max(64));
|
||||||
|
s.h = s.h.clamp(64, m.height.max(64));
|
||||||
|
let max_x = m.x + m.width - s.w;
|
||||||
|
let max_y = m.y + m.height - s.h;
|
||||||
|
s.x = s.x.clamp(m.x.min(max_x), max_x.max(m.x));
|
||||||
|
s.y = s.y.clamp(m.y.min(max_y), max_y.max(m.y));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn constrain(w: i32, h: i32) -> (i32, i32) {
|
fn constrain(w: i32, h: i32) -> (i32, i32) {
|
||||||
@ -201,10 +214,22 @@ pub async fn apply_for_profile(profile: &mut Profile) -> Result<usize> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn apply(slots: &[LayoutSlot], windows: &[(u32, Client)]) -> Result<()> {
|
pub async fn apply(slots: &[LayoutSlot], windows: &[(u32, Client)]) -> Result<()> {
|
||||||
|
let mons = monitors().await.unwrap_or_default();
|
||||||
for (i, win) in windows {
|
for (i, win) in windows {
|
||||||
let Some(geom) = slots.get((*i as usize).saturating_sub(1)) else {
|
let Some(geom0) = slots.get((*i as usize).saturating_sub(1)) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let mut geom = geom0.clone();
|
||||||
|
if let Some(m) = mons.iter().find(|m| {
|
||||||
|
geom.x >= m.x && geom.y >= m.y && geom.x < m.x + m.width && geom.y < m.y + m.height
|
||||||
|
}) {
|
||||||
|
clamp_to_monitor(&mut geom, m);
|
||||||
|
} else if let Some(m) = mons.first() {
|
||||||
|
clamp_to_monitor(&mut geom, m);
|
||||||
|
}
|
||||||
|
if geom.w < 64 || geom.h < 64 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let sel = win.address_selector();
|
let sel = win.address_selector();
|
||||||
hypr::dispatch_lua(&format!(
|
hypr::dispatch_lua(&format!(
|
||||||
"hl.dsp.window.float({{ window = {sel:?}, action = \"on\" }})"
|
"hl.dsp.window.float({{ window = {sel:?}, action = \"on\" }})"
|
||||||
@ -266,7 +291,25 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let s = generate(&l, 3, &[mon()]);
|
let s = generate(&l, 3, &[mon()]);
|
||||||
assert_eq!(s.len(), 3);
|
assert_eq!(s.len(), 3);
|
||||||
assert!(s.iter().all(|w| w.w == 1920 && w.h == 1080 && w.x == 0));
|
assert!(s
|
||||||
|
.iter()
|
||||||
|
.all(|w| w.w == 1920 && w.h == 1080 && w.x == 0 && w.y == 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clamp_rejects_negative() {
|
||||||
|
let m = mon();
|
||||||
|
let mut s = LayoutSlot {
|
||||||
|
x: -400,
|
||||||
|
y: -500,
|
||||||
|
w: 1280,
|
||||||
|
h: 1440,
|
||||||
|
pin: false,
|
||||||
|
};
|
||||||
|
clamp_to_monitor(&mut s, &m);
|
||||||
|
assert!(s.x >= 0 && s.y >= 0);
|
||||||
|
assert!(s.x + s.w <= m.width);
|
||||||
|
assert!(s.y + s.h <= m.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -280,6 +323,29 @@ mod tests {
|
|||||||
assert!(s.iter().all(|w| w.w > 0 && w.h > 0));
|
assert!(s.iter().all(|w| w.w > 0 && w.h > 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swap_main_exchanges_first() {
|
||||||
|
let mut s = vec![
|
||||||
|
LayoutSlot {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 100,
|
||||||
|
h: 100,
|
||||||
|
pin: true,
|
||||||
|
},
|
||||||
|
LayoutSlot {
|
||||||
|
x: 100,
|
||||||
|
y: 0,
|
||||||
|
w: 50,
|
||||||
|
h: 50,
|
||||||
|
pin: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
swap_main(&mut s, 1);
|
||||||
|
assert_eq!(s[0].w, 50);
|
||||||
|
assert_eq!(s[1].w, 100);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn main_strip_has_big_first() {
|
fn main_strip_has_big_first() {
|
||||||
let l = Layout {
|
let l = Layout {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ pub struct Profile {
|
|||||||
pub game_binds: BTreeMap<String, String>,
|
pub game_binds: BTreeMap<String, String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub interact: Interact,
|
pub interact: Interact,
|
||||||
#[serde(default)]
|
#[serde(default = "default_session_hotkeys")]
|
||||||
pub session_hotkeys: BTreeMap<String, String>,
|
pub session_hotkeys: BTreeMap<String, String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub characters: Vec<Character>,
|
pub characters: Vec<Character>,
|
||||||
@ -44,6 +44,21 @@ fn default_client() -> String {
|
|||||||
fn default_slots() -> u32 {
|
fn default_slots() -> u32 {
|
||||||
2
|
2
|
||||||
}
|
}
|
||||||
|
fn default_session_hotkeys() -> BTreeMap<String, String> {
|
||||||
|
let mut m = BTreeMap::new();
|
||||||
|
m.insert("mode_cycle".into(), "Shift+Alt+M".into());
|
||||||
|
m.insert("swap_next".into(), "Ctrl+grave".into());
|
||||||
|
m.insert("swap_prev".into(), "Ctrl+Shift+grave".into());
|
||||||
|
m.insert("focus_next".into(), "Ctrl+Shift+N".into());
|
||||||
|
m.insert("focus_prev".into(), "Ctrl+Shift+P".into());
|
||||||
|
m.insert("focus_main".into(), "Ctrl+F1".into());
|
||||||
|
m.insert("reset_all".into(), "Ctrl+Shift+R".into());
|
||||||
|
m.insert("stay_on_top".into(), "Ctrl+Shift+T".into());
|
||||||
|
m.insert("mouse_follow".into(), "Ctrl+Shift+F".into());
|
||||||
|
m.insert("mouse_broadcast".into(), "Ctrl+Shift+B".into());
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
fn default_mode() -> Mode {
|
fn default_mode() -> Mode {
|
||||||
Mode::Maps
|
Mode::Maps
|
||||||
}
|
}
|
||||||
@ -331,7 +346,10 @@ impl Profile {
|
|||||||
pub fn load(path: &Path) -> Result<Self> {
|
pub fn load(path: &Path) -> Result<Self> {
|
||||||
let text = std::fs::read_to_string(path)
|
let text = std::fs::read_to_string(path)
|
||||||
.with_context(|| format!("read profile {}", path.display()))?;
|
.with_context(|| format!("read profile {}", path.display()))?;
|
||||||
let profile: Profile = serde_yaml::from_str(&text).context("parse profile YAML")?;
|
let mut profile: Profile = serde_yaml::from_str(&text).context("parse profile YAML")?;
|
||||||
|
for (k, v) in default_session_hotkeys() {
|
||||||
|
profile.session_hotkeys.entry(k).or_insert(v);
|
||||||
|
}
|
||||||
profile.validate()?;
|
profile.validate()?;
|
||||||
Ok(profile)
|
Ok(profile)
|
||||||
}
|
}
|
||||||
|
|||||||
288
src/session.rs
288
src/session.rs
@ -20,6 +20,8 @@ pub struct Session {
|
|||||||
pub sock: PathBuf,
|
pub sock: PathBuf,
|
||||||
pub vfx: Vec<FeedHit>,
|
pub vfx: Vec<FeedHit>,
|
||||||
pub vfx_source: Option<u32>,
|
pub vfx_source: Option<u32>,
|
||||||
|
pub mouse_broadcast: bool,
|
||||||
|
pub mouse_follow: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
@ -32,6 +34,8 @@ impl Session {
|
|||||||
sock,
|
sock,
|
||||||
vfx: Vec::new(),
|
vfx: Vec::new(),
|
||||||
vfx_source: None,
|
vfx_source: None,
|
||||||
|
mouse_broadcast: false,
|
||||||
|
mouse_follow: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -325,15 +329,64 @@ fn bind_specs(g: &Session) -> Result<Vec<BindSpec>> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(cycle) = g.engine.profile.session_hotkeys.get("mode_cycle") {
|
let wm = [
|
||||||
let parsed = hotkey::parse(cycle)?;
|
("mode_cycle", "mode-cycle"),
|
||||||
specs.push(BindSpec {
|
("swap_next", "swap-next"),
|
||||||
bind: parsed.hypr_bind(),
|
("swap_prev", "swap-prev"),
|
||||||
ipc_bin: bin.clone(),
|
("focus_next", "focus-next"),
|
||||||
ipc_args: "mode-cycle".into(),
|
("focus_prev", "focus-prev"),
|
||||||
non_consuming: false,
|
("focus_main", "focus-main"),
|
||||||
release: false,
|
("reset_all", "reset-all"),
|
||||||
});
|
("stay_on_top", "stay-on-top"),
|
||||||
|
("mouse_follow", "mouse-follow"),
|
||||||
|
("mouse_broadcast", "mouse-broadcast"),
|
||||||
|
];
|
||||||
|
for (name, ipc) in wm {
|
||||||
|
if let Some(hk) = g.engine.profile.session_hotkeys.get(name) {
|
||||||
|
let parsed = hotkey::parse(hk)?;
|
||||||
|
specs.push(BindSpec {
|
||||||
|
bind: parsed.hypr_bind(),
|
||||||
|
ipc_bin: bin.clone(),
|
||||||
|
ipc_args: ipc.into(),
|
||||||
|
non_consuming: false,
|
||||||
|
release: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i in 1..=g.engine.profile.slots {
|
||||||
|
let focus = format!("Ctrl+F{i}");
|
||||||
|
let swap = format!("Ctrl+Shift+F{i}");
|
||||||
|
if let Ok(p) = hotkey::parse(&focus) {
|
||||||
|
specs.push(BindSpec {
|
||||||
|
bind: p.hypr_bind(),
|
||||||
|
ipc_bin: bin.clone(),
|
||||||
|
ipc_args: format!("focus {i}"),
|
||||||
|
non_consuming: false,
|
||||||
|
release: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if i > 1 {
|
||||||
|
if let Ok(p) = hotkey::parse(&swap) {
|
||||||
|
specs.push(BindSpec {
|
||||||
|
bind: p.hypr_bind(),
|
||||||
|
ipc_bin: bin.clone(),
|
||||||
|
ipc_args: format!("swap {i}"),
|
||||||
|
non_consuming: false,
|
||||||
|
release: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if g.mouse_broadcast {
|
||||||
|
for btn in ["mouse:272", "mouse:273"] {
|
||||||
|
specs.push(BindSpec {
|
||||||
|
bind: btn.to_string(),
|
||||||
|
ipc_bin: bin.clone(),
|
||||||
|
ipc_args: format!("mouse-click {btn}"),
|
||||||
|
non_consuming: true,
|
||||||
|
release: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if g.vfx_source.is_some() {
|
if g.vfx_source.is_some() {
|
||||||
for k in vfx_hover_keys() {
|
for k in vfx_hover_keys() {
|
||||||
@ -372,11 +425,13 @@ async fn dispatch_cmd(session: &Arc<Mutex<Session>>, line: &str) -> String {
|
|||||||
"status" => {
|
"status" => {
|
||||||
let g = session.lock().await;
|
let g = session.lock().await;
|
||||||
format!(
|
format!(
|
||||||
"mode={} leader={} binds={} slots={} vfx_source={:?}",
|
"mode={} leader={} binds={} slots={} mouse_broadcast={} mouse_follow={} vfx_source={:?}",
|
||||||
g.engine.mode.as_str(),
|
g.engine.mode.as_str(),
|
||||||
g.engine.leader_slot,
|
g.engine.leader_slot,
|
||||||
g.binds_on,
|
g.binds_on,
|
||||||
g.slots.len(),
|
g.slots.len(),
|
||||||
|
g.mouse_broadcast,
|
||||||
|
g.mouse_follow,
|
||||||
g.vfx_source
|
g.vfx_source
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -412,6 +467,30 @@ async fn dispatch_cmd(session: &Arc<Mutex<Session>>, line: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"swap-next" => wm_ok(swap_next(session).await),
|
||||||
|
"swap-prev" => wm_ok(swap_prev(session).await),
|
||||||
|
"swap" => match arg.parse::<u32>() {
|
||||||
|
Ok(n) => wm_ok(swap_as_main(session, n).await),
|
||||||
|
Err(_) => "err bad slot".into(),
|
||||||
|
},
|
||||||
|
"focus-next" => wm_ok(focus_step(session, 1).await),
|
||||||
|
"focus-prev" => wm_ok(focus_step(session, -1).await),
|
||||||
|
"focus-main" => wm_ok(focus_slot(session, 1).await),
|
||||||
|
"focus" => match arg.parse::<u32>() {
|
||||||
|
Ok(n) => wm_ok(focus_slot(session, n).await),
|
||||||
|
Err(_) => "err bad slot".into(),
|
||||||
|
},
|
||||||
|
"reset-all" => wm_ok(reset_layout(session).await),
|
||||||
|
"stay-on-top" => wm_ok(toggle_pin(session).await),
|
||||||
|
"mouse-follow" => wm_ok(toggle_mouse_follow(session).await),
|
||||||
|
"mouse-broadcast" => wm_ok(toggle_mouse_broadcast(session).await),
|
||||||
|
"mouse-click" => {
|
||||||
|
let btn = arg
|
||||||
|
.strip_prefix("mouse:")
|
||||||
|
.and_then(|s| s.parse::<u32>().ok())
|
||||||
|
.unwrap_or(272);
|
||||||
|
wm_ok(broadcast_click(session, btn).await)
|
||||||
|
}
|
||||||
_ => format!("err unknown {cmd}"),
|
_ => format!("err unknown {cmd}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -447,6 +526,195 @@ async fn set_mode(session: &Arc<Mutex<Session>>, name: Option<&str>) -> Result<M
|
|||||||
Ok(mode)
|
Ok(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wm_ok(r: Result<()>) -> String {
|
||||||
|
match r {
|
||||||
|
Ok(()) => "ok".into(),
|
||||||
|
Err(e) => format!("err {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn swap_next(session: &Arc<Mutex<Session>>) -> Result<()> {
|
||||||
|
let n = {
|
||||||
|
let g = session.lock().await;
|
||||||
|
let len = g.slots.len() as u32;
|
||||||
|
if len < 2 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let cur = g.engine.leader_slot;
|
||||||
|
if cur >= len {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
cur + 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
swap_as_main(session, n.max(2)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn swap_prev(session: &Arc<Mutex<Session>>) -> Result<()> {
|
||||||
|
let n = {
|
||||||
|
let g = session.lock().await;
|
||||||
|
let len = g.slots.len() as u32;
|
||||||
|
if len < 2 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let cur = g.engine.leader_slot;
|
||||||
|
if cur <= 2 {
|
||||||
|
len
|
||||||
|
} else {
|
||||||
|
cur - 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
swap_as_main(session, n).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn swap_as_main(session: &Arc<Mutex<Session>>, n: u32) -> Result<()> {
|
||||||
|
let (wins, tiles) = {
|
||||||
|
let mut g = session.lock().await;
|
||||||
|
if n < 2 || n as usize > g.slots.len() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
g.slots.swap(0, n as usize - 1);
|
||||||
|
for (i, (id, _)) in g.slots.iter_mut().enumerate() {
|
||||||
|
*id = i as u32 + 1;
|
||||||
|
}
|
||||||
|
g.engine.set_leader(1);
|
||||||
|
(g.slots.clone(), g.engine.profile.layout.slots.clone())
|
||||||
|
};
|
||||||
|
if !tiles.is_empty() {
|
||||||
|
crate::layout::apply(&tiles, &wins).await?;
|
||||||
|
}
|
||||||
|
if let Some((_, c)) = wins.first() {
|
||||||
|
hypr::focus_window(&c.address_selector()).await.ok();
|
||||||
|
}
|
||||||
|
hypr::notify(&format!("main is slot 1 (was {n})"))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn focus_step(session: &Arc<Mutex<Session>>, dir: i32) -> Result<()> {
|
||||||
|
let n = {
|
||||||
|
let g = session.lock().await;
|
||||||
|
let len = g.slots.len() as i32;
|
||||||
|
if len == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let cur = g.engine.leader_slot as i32;
|
||||||
|
let next = ((cur - 1 + dir).rem_euclid(len)) + 1;
|
||||||
|
next as u32
|
||||||
|
};
|
||||||
|
focus_slot(session, n).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn focus_slot(session: &Arc<Mutex<Session>>, n: u32) -> Result<()> {
|
||||||
|
let addr = {
|
||||||
|
let mut g = session.lock().await;
|
||||||
|
g.engine.set_leader(n);
|
||||||
|
g.slots
|
||||||
|
.iter()
|
||||||
|
.find(|(s, _)| *s == n)
|
||||||
|
.map(|(_, c)| c.address_selector())
|
||||||
|
};
|
||||||
|
if let Some(a) = addr {
|
||||||
|
hypr::focus_window(&a).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reset_layout(session: &Arc<Mutex<Session>>) -> Result<()> {
|
||||||
|
let (wins, tiles) = {
|
||||||
|
let g = session.lock().await;
|
||||||
|
(g.slots.clone(), g.engine.profile.layout.slots.clone())
|
||||||
|
};
|
||||||
|
if tiles.is_empty() {
|
||||||
|
anyhow::bail!("no saved layout tiles");
|
||||||
|
}
|
||||||
|
crate::layout::apply(&tiles, &wins).await?;
|
||||||
|
hypr::notify("layout reset").await.ok();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn toggle_pin(session: &Arc<Mutex<Session>>) -> Result<()> {
|
||||||
|
let addr = {
|
||||||
|
let g = session.lock().await;
|
||||||
|
g.slots
|
||||||
|
.iter()
|
||||||
|
.find(|(s, _)| *s == g.engine.leader_slot)
|
||||||
|
.map(|(_, c)| c.address_selector())
|
||||||
|
};
|
||||||
|
if let Some(a) = addr {
|
||||||
|
hypr::dispatch_lua(&format!(
|
||||||
|
"hl.dsp.window.pin({{ window = {a:?}, action = \"toggle\" }})"
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn toggle_mouse_follow(session: &Arc<Mutex<Session>>) -> Result<()> {
|
||||||
|
let on = {
|
||||||
|
let mut g = session.lock().await;
|
||||||
|
g.mouse_follow = !g.mouse_follow;
|
||||||
|
g.mouse_follow
|
||||||
|
};
|
||||||
|
let v = if on { 1 } else { 0 };
|
||||||
|
let _ = hypr::hyprctl(["keyword", "input:follow_mouse", &v.to_string()]).await;
|
||||||
|
hypr::notify(&format!(
|
||||||
|
"focus follows mouse {}",
|
||||||
|
if on { "on" } else { "off" }
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn toggle_mouse_broadcast(session: &Arc<Mutex<Session>>) -> Result<()> {
|
||||||
|
{
|
||||||
|
let mut g = session.lock().await;
|
||||||
|
g.mouse_broadcast = !g.mouse_broadcast;
|
||||||
|
g.binds_on = false;
|
||||||
|
hypr::notify(&format!(
|
||||||
|
"mouse broadcast {}",
|
||||||
|
if g.mouse_broadcast { "on" } else { "off" }
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn broadcast_click(session: &Arc<Mutex<Session>>, button: u32) -> Result<()> {
|
||||||
|
let (cx, cy) = hypr::cursor_pos().await?;
|
||||||
|
let others = {
|
||||||
|
let g = session.lock().await;
|
||||||
|
if !g.mouse_broadcast {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let Some((_, primary)) = g.slots.iter().find(|(s, _)| *s == g.engine.leader_slot) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let pw = primary.size[0].max(1) as f64;
|
||||||
|
let ph = primary.size[1].max(1) as f64;
|
||||||
|
let nx = (cx - primary.at[0]) as f64 / pw;
|
||||||
|
let ny = (cy - primary.at[1]) as f64 / ph;
|
||||||
|
g.slots
|
||||||
|
.iter()
|
||||||
|
.filter(|(s, _)| *s != g.engine.leader_slot)
|
||||||
|
.map(|(_, c)| {
|
||||||
|
let x = c.at[0] + (nx * c.size[0] as f64).round() as i32;
|
||||||
|
let y = c.at[1] + (ny * c.size[1] as f64).round() as i32;
|
||||||
|
(c.clone(), x, y)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
for (c, x, y) in others {
|
||||||
|
hypr::move_cursor(x, y).await.ok();
|
||||||
|
hypr::deliver_click(&c, button).await.ok();
|
||||||
|
}
|
||||||
|
hypr::move_cursor(cx, cy).await.ok();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn clone_key_set() -> Vec<String> {
|
fn clone_key_set() -> Vec<String> {
|
||||||
vfx_hover_keys()
|
vfx_hover_keys()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user