Round 5 batch B (Grok review): live wiring for items 2, 4, 5, 6, 7, 9.
Grok round 5 found these surfaces either dead or lying: 2. Item 4 GUI never shipped. page_session had no game dropdown; menu_launch was still Lutris. Added a Game row: ComboBox fed by the daemon `list-games` verb (refresh_games helper), Launch button fires `launch-game NAME`. New App fields game_list / game_pick. 4. GUI local spawn never filled Session.spawned_pids. spawn_plan now captures the child PID and pushes it via the new `track-pid` IPC verb; status shows the pid. Daemon-side track-pid inserts into Session.spawned_pids. 5. launch_game drop(Child) did not reap -> zombies (wine wrappers exit fast). Replaced with a background thread that calls child.wait(); the PID stays in spawned_pids. 6. Deleted the select_windows placeholder that returned a fake first-N-visible list disagreeing with the daemon assignment. select_windows now reads the daemon slot list via the new `slots` IPC verb (layout::slots_from_daemon) and returns the same (id, Client) shape. 7. resize_slot / move_slot used slot.saturating_sub(1) as an index into layout.slots, which breaks when Session.slots has gaps after a hide ([1a, 3b] -> resize-slot 3 bailed). LayoutSlot gains a stable `id: u32` field; both handlers now find the layout slot by id. LayoutSlot literals across the repo given an id. 9. keybind_detail Map panel now exposes release_steps (Add / Remove / edit per entry), matching the press steps editor. cargo test 103/103; clippy clean.
This commit is contained in:
parent
5aabf0d36f
commit
677ae0d3dc
99
src/gui.rs
99
src/gui.rs
@ -61,6 +61,8 @@ pub fn run() -> Result<()> {
|
|||||||
profile_names_list: Vec::new(),
|
profile_names_list: Vec::new(),
|
||||||
teams: AppTeams::default(),
|
teams: AppTeams::default(),
|
||||||
selected_node: None,
|
selected_node: None,
|
||||||
|
game_list: Vec::new(),
|
||||||
|
game_pick: String::new(),
|
||||||
};
|
};
|
||||||
eframe::run_native("enBoxer", native, Box::new(|_cc| Ok(Box::new(app))))
|
eframe::run_native("enBoxer", native, Box::new(|_cc| Ok(Box::new(app))))
|
||||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
@ -122,6 +124,10 @@ struct App {
|
|||||||
teams: AppTeams,
|
teams: AppTeams,
|
||||||
/// Item 2: which tree node is currently selected.
|
/// Item 2: which tree node is currently selected.
|
||||||
selected_node: Option<SelectedNode>,
|
selected_node: Option<SelectedNode>,
|
||||||
|
/// Round-5 item 2: games.yaml names, refreshed on demand.
|
||||||
|
game_list: Vec<String>,
|
||||||
|
/// Round-5 item 2: currently picked name in the dropdown.
|
||||||
|
game_pick: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -174,6 +180,29 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Round-5 item 2: refresh the games dropdown from the daemon.
|
||||||
|
/// Uses `list-games` which returns one name per line.
|
||||||
|
fn refresh_games(&mut self) {
|
||||||
|
let sock = session::default_sock();
|
||||||
|
let mut cmd = Command::new(Self::exe());
|
||||||
|
cmd.arg("ipc").arg("--sock").arg(&sock).arg("list-games");
|
||||||
|
match cmd.output() {
|
||||||
|
Ok(o) => {
|
||||||
|
let text = String::from_utf8_lossy(&o.stdout);
|
||||||
|
self.game_list = text
|
||||||
|
.lines()
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
if self.game_pick.is_empty() {
|
||||||
|
self.game_pick = self.game_list.first().cloned().unwrap_or_default();
|
||||||
|
}
|
||||||
|
self.status = format!("{} game(s) available", self.game_list.len());
|
||||||
|
}
|
||||||
|
Err(e) => self.error = Some(format!("list-games: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn profiles_dir() -> PathBuf {
|
fn profiles_dir() -> PathBuf {
|
||||||
default_config_path()
|
default_config_path()
|
||||||
.parent()
|
.parent()
|
||||||
@ -657,6 +686,30 @@ impl App {
|
|||||||
ui.label("Profile name");
|
ui.label("Profile name");
|
||||||
ui.text_edit_singleline(&mut self.profile.name);
|
ui.text_edit_singleline(&mut self.profile.name);
|
||||||
});
|
});
|
||||||
|
// Round-5 item 2: game-launcher dropdown. The daemon reads
|
||||||
|
// ~/.config/enboxer/games.yaml and exposes the names via
|
||||||
|
// the `list-games` IPC verb. Picking one fires
|
||||||
|
// `launch-game NAME`, which spawns the entry and records
|
||||||
|
// the child PID into Session.spawned_pids so refresh_slots
|
||||||
|
// can match the resulting window via the process tree.
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Game");
|
||||||
|
let games = self.game_list.clone();
|
||||||
|
egui::ComboBox::from_label("launch")
|
||||||
|
.selected_text(self.game_pick.clone())
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for name in &games {
|
||||||
|
ui.selectable_value(&mut self.game_pick, name.clone(), name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if ui.button("Refresh list").clicked() {
|
||||||
|
self.refresh_games();
|
||||||
|
}
|
||||||
|
if ui.button("Launch").clicked() && !self.game_pick.is_empty() {
|
||||||
|
let name = self.game_pick.clone();
|
||||||
|
self.ipc("launch-game", &name);
|
||||||
|
}
|
||||||
|
});
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Slots");
|
ui.label("Slots");
|
||||||
ui.add(egui::DragValue::new(&mut self.profile.slots).range(1..=16));
|
ui.add(egui::DragValue::new(&mut self.profile.slots).range(1..=16));
|
||||||
@ -1190,6 +1243,7 @@ impl App {
|
|||||||
while self.profile.layout.slots.len() < self.profile.slots as usize {
|
while self.profile.layout.slots.len() < self.profile.slots as usize {
|
||||||
self.profile.layout.slots.push(crate::profile::LayoutSlot {
|
self.profile.layout.slots.push(crate::profile::LayoutSlot {
|
||||||
x: 0,
|
x: 0,
|
||||||
|
id: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
w: 800,
|
w: 800,
|
||||||
h: 600,
|
h: 600,
|
||||||
@ -1299,6 +1353,7 @@ impl App {
|
|||||||
while self.profile.layout.slots.len() < self.profile.slots as usize {
|
while self.profile.layout.slots.len() < self.profile.slots as usize {
|
||||||
self.profile.layout.slots.push(crate::profile::LayoutSlot {
|
self.profile.layout.slots.push(crate::profile::LayoutSlot {
|
||||||
x: 0,
|
x: 0,
|
||||||
|
id: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
w: 800,
|
w: 800,
|
||||||
h: 600,
|
h: 600,
|
||||||
@ -1484,6 +1539,40 @@ impl App {
|
|||||||
target: "all".to_string(),
|
target: "all".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
ui.separator();
|
||||||
|
ui.label("Release steps (executed when the hotkey is released)");
|
||||||
|
let mut release_remove = None;
|
||||||
|
for (ri, rs) in m.release_steps.iter_mut().enumerate() {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(format!("{}", ri + 1));
|
||||||
|
ui.label("bind");
|
||||||
|
let mut bind_str = rs.bind.clone().unwrap_or_default();
|
||||||
|
if ui.text_edit_singleline(&mut bind_str).changed() {
|
||||||
|
rs.bind = if bind_str.is_empty() { None } else { Some(bind_str) };
|
||||||
|
}
|
||||||
|
ui.label("key");
|
||||||
|
let mut key_str = rs.key.clone().unwrap_or_default();
|
||||||
|
if ui.text_edit_singleline(&mut key_str).changed() {
|
||||||
|
rs.key = if key_str.is_empty() { None } else { Some(key_str) };
|
||||||
|
}
|
||||||
|
ui.label("target");
|
||||||
|
ui.text_edit_singleline(&mut rs.target);
|
||||||
|
if ui.button("x").clicked() {
|
||||||
|
release_remove = Some(ri);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(ri) = release_remove {
|
||||||
|
m.release_steps.remove(ri);
|
||||||
|
}
|
||||||
|
if ui.button("+ Add release step").clicked() {
|
||||||
|
m.release_steps.push(crate::profile::Step {
|
||||||
|
bind: None,
|
||||||
|
key: None,
|
||||||
|
delay_ms: None,
|
||||||
|
target: "all".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(SelectedNode::VideoFx(i)) => {
|
Some(SelectedNode::VideoFx(i)) => {
|
||||||
@ -1877,8 +1966,14 @@ impl App {
|
|||||||
cmd.env(k, v);
|
cmd.env(k, v);
|
||||||
}
|
}
|
||||||
match cmd.spawn() {
|
match cmd.spawn() {
|
||||||
Ok(_child) => {
|
Ok(child) => {
|
||||||
self.status = plan.summary.clone();
|
let pid = child.id();
|
||||||
|
self.status = format!("{} (pid {})", plan.summary, pid);
|
||||||
|
// Round-5 Item 4: push the PID into the daemon's
|
||||||
|
// Session.spawned_pids via the track-pid IPC verb
|
||||||
|
// so refresh_slots can match the resulting window
|
||||||
|
// via the process-tree walk.
|
||||||
|
self.ipc("track-pid", &pid.to_string());
|
||||||
if ch.auto_apply {
|
if ch.auto_apply {
|
||||||
self.status.push_str(" (auto-apply armed)");
|
self.status.push_str(" (auto-apply armed)");
|
||||||
self.arm_auto_apply(ch.slot);
|
self.arm_auto_apply(ch.slot);
|
||||||
|
|||||||
@ -53,6 +53,7 @@ pub fn generate(layout: &Layout, n: u32, mons: &[Monitor]) -> Vec<LayoutSlot> {
|
|||||||
(0..n)
|
(0..n)
|
||||||
.map(|_| LayoutSlot {
|
.map(|_| LayoutSlot {
|
||||||
x: m.x,
|
x: m.x,
|
||||||
|
id: 0,
|
||||||
y: m.y,
|
y: m.y,
|
||||||
w,
|
w,
|
||||||
h,
|
h,
|
||||||
@ -95,6 +96,7 @@ fn grid(m: &Monitor, n: u32, pin: bool) -> Vec<LayoutSlot> {
|
|||||||
}
|
}
|
||||||
out.push(LayoutSlot {
|
out.push(LayoutSlot {
|
||||||
x: m.x + x as i32 * ww,
|
x: m.x + x as i32 * ww,
|
||||||
|
id: 0,
|
||||||
y: m.y + y as i32 * wh,
|
y: m.y + y as i32 * wh,
|
||||||
w: ww,
|
w: ww,
|
||||||
h: wh,
|
h: wh,
|
||||||
@ -130,6 +132,7 @@ fn main_strip(m: &Monitor, n: u32, layout: &Layout) -> Vec<LayoutSlot> {
|
|||||||
};
|
};
|
||||||
out.push(LayoutSlot {
|
out.push(LayoutSlot {
|
||||||
x: m.x,
|
x: m.x,
|
||||||
|
id: 0,
|
||||||
y: main_y,
|
y: main_y,
|
||||||
w: bw,
|
w: bw,
|
||||||
h: bh,
|
h: bh,
|
||||||
@ -154,6 +157,7 @@ fn main_strip(m: &Monitor, n: u32, layout: &Layout) -> Vec<LayoutSlot> {
|
|||||||
};
|
};
|
||||||
out.push(LayoutSlot {
|
out.push(LayoutSlot {
|
||||||
x,
|
x,
|
||||||
|
id: 0,
|
||||||
y,
|
y,
|
||||||
w: sw.max(1),
|
w: sw.max(1),
|
||||||
h: sh.max(1),
|
h: sh.max(1),
|
||||||
@ -164,25 +168,55 @@ fn main_strip(m: &Monitor, n: u32, layout: &Layout) -> Vec<LayoutSlot> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
// Item 3: select_windows is now a placeholder. The real picker
|
// Grok round 5 Item 6: select_windows used to return a fake
|
||||||
// runs in `session::refresh_slots` against `Session.spawned_pids`.
|
// first-N-visible list that disagreed with the daemon's real
|
||||||
// This stub returns the first `profile.slots` visible clients in
|
// slot assignment (stable IDs from process-tree walk in
|
||||||
// z-order so layout code that still calls select_windows during
|
// refresh_slots). Callers must now read the slot list from the
|
||||||
// the rest of the Item 3 rollout does not silently lose every
|
// daemon over IPC so the GUI's view matches what refresh_slots
|
||||||
// window. It will be deleted once refresh_slots fully owns slot
|
// produced. This helper shells out to `enboxer ipc slots` and
|
||||||
// assignment.
|
// parses the JSON response.
|
||||||
pub fn select_windows(_profile: &Profile, clients: Vec<Client>) -> Vec<(u32, Client)> {
|
pub fn select_windows(_profile: &Profile, _clients: Vec<Client>) -> Vec<(u32, Client)> {
|
||||||
let mut visible: Vec<Client> = clients
|
crate::layout::slots_from_daemon().unwrap_or_default()
|
||||||
.into_iter()
|
}
|
||||||
.filter(|c| c.mapped && !c.hidden && c.class != "enboxer-vfx")
|
|
||||||
.collect();
|
/// Item 6: ask the daemon for the current slot assignments.
|
||||||
visible.sort_by_key(|c| (c.at[1], c.at[0], c.pid));
|
/// Returns a `Vec<(slot_id, Client)>` so the same shape as
|
||||||
visible
|
/// `select_windows` is preserved.
|
||||||
.into_iter()
|
pub fn slots_from_daemon() -> anyhow::Result<Vec<(u32, Client)>> {
|
||||||
.take(_profile.slots as usize)
|
let sock = crate::session::default_sock();
|
||||||
.enumerate()
|
let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("enboxer"));
|
||||||
.map(|(i, c)| ((i as u32) + 1, c))
|
let out = std::process::Command::new(exe)
|
||||||
.collect()
|
.arg("ipc").arg("--sock").arg(&sock).arg("slots").output()?;
|
||||||
|
if !out.status.success() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let text = String::from_utf8_lossy(&out.stdout);
|
||||||
|
// The daemon returns one slot per line:
|
||||||
|
// <id> <address> <pid> <class> <title>
|
||||||
|
// We only need (id, Client) for select_windows.
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for line in text.lines() {
|
||||||
|
let mut it = line.split_whitespace();
|
||||||
|
if let (Some(id), Some(addr), Some(pid)) = (it.next(), it.next(), it.next()) {
|
||||||
|
let class = it.next().unwrap_or("").to_string();
|
||||||
|
let title = it.collect::<Vec<_>>().join(" ");
|
||||||
|
if let (Ok(id), Ok(pid)) = (id.parse::<u32>(), pid.parse::<i32>()) {
|
||||||
|
out.push((id, Client {
|
||||||
|
address: addr.to_string(),
|
||||||
|
pid,
|
||||||
|
class,
|
||||||
|
title,
|
||||||
|
mapped: true,
|
||||||
|
hidden: false,
|
||||||
|
at: [0, 0],
|
||||||
|
size: [0, 0],
|
||||||
|
xwayland: false,
|
||||||
|
focus_history_id: 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Window move/resize/pin is off unless ENBOXER_ALLOW_LAYOUT=1.
|
/// Window move/resize/pin is off unless ENBOXER_ALLOW_LAYOUT=1.
|
||||||
@ -248,6 +282,7 @@ pub fn capture_from(windows: &[(u32, Client)]) -> Vec<LayoutSlot> {
|
|||||||
for (_, c) in windows {
|
for (_, c) in windows {
|
||||||
out.push(LayoutSlot {
|
out.push(LayoutSlot {
|
||||||
x: c.at[0],
|
x: c.at[0],
|
||||||
|
id: 0,
|
||||||
y: c.at[1],
|
y: c.at[1],
|
||||||
w: c.size[0],
|
w: c.size[0],
|
||||||
h: c.size[1],
|
h: c.size[1],
|
||||||
@ -307,6 +342,7 @@ mod tests {
|
|||||||
let m = mon();
|
let m = mon();
|
||||||
let mut s = LayoutSlot {
|
let mut s = LayoutSlot {
|
||||||
x: -400,
|
x: -400,
|
||||||
|
id: 0,
|
||||||
y: -500,
|
y: -500,
|
||||||
w: 1440,
|
w: 1440,
|
||||||
h: 1440,
|
h: 1440,
|
||||||
@ -335,6 +371,7 @@ mod tests {
|
|||||||
let mut s = vec![
|
let mut s = vec![
|
||||||
LayoutSlot {
|
LayoutSlot {
|
||||||
x: 0,
|
x: 0,
|
||||||
|
id: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
w: 100,
|
w: 100,
|
||||||
h: 100,
|
h: 100,
|
||||||
@ -343,6 +380,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
LayoutSlot {
|
LayoutSlot {
|
||||||
x: 100,
|
x: 100,
|
||||||
|
id: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
w: 50,
|
w: 50,
|
||||||
h: 50,
|
h: 50,
|
||||||
|
|||||||
@ -289,6 +289,12 @@ pub struct Layout {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct LayoutSlot {
|
pub struct LayoutSlot {
|
||||||
|
/// Round-5 Item 7: stable slot id used to look up this slot
|
||||||
|
/// by ID (Session.slots can have gaps after hides). Defaults
|
||||||
|
/// to 0 which means "unbound"; the GUI sets it when creating
|
||||||
|
/// a layout slot.
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: u32,
|
||||||
/// Item 1: cached last-known good geometry, used by Managed
|
/// Item 1: cached last-known good geometry, used by Managed
|
||||||
/// mode. Ignored by Free mode.
|
/// mode. Ignored by Free mode.
|
||||||
pub x: i32,
|
pub x: i32,
|
||||||
|
|||||||
124
src/session.rs
124
src/session.rs
@ -574,6 +574,26 @@ async fn dispatch_cmd(session: &Arc<Mutex<Session>>, line: &str) -> String {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
games.iter().map(|g| g.name.clone()).collect::<Vec<_>>().join("\n")
|
games.iter().map(|g| g.name.clone()).collect::<Vec<_>>().join("\n")
|
||||||
}
|
}
|
||||||
|
"slots" => {
|
||||||
|
let g = session.lock().await;
|
||||||
|
g.slots
|
||||||
|
.iter()
|
||||||
|
.map(|(id, c)| {
|
||||||
|
format!(
|
||||||
|
"{} 0x{} {} {} {}",
|
||||||
|
id, c.address, c.pid, c.class, c.title
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
"track-pid" => {
|
||||||
|
if let Ok(pid) = arg.parse::<u32>() {
|
||||||
|
let mut g = session.lock().await;
|
||||||
|
g.spawned_pids.insert(pid);
|
||||||
|
}
|
||||||
|
"ok".to_string()
|
||||||
|
}
|
||||||
"resize-slot" => {
|
"resize-slot" => {
|
||||||
let n = arg.parse::<u32>().unwrap_or(0);
|
let n = arg.parse::<u32>().unwrap_or(0);
|
||||||
wm_ok(resize_slot(session, n).await)
|
wm_ok(resize_slot(session, n).await)
|
||||||
@ -946,7 +966,7 @@ pub async fn launch_game(session: &Arc<Mutex<Session>>, name: &str) -> Result<()
|
|||||||
for (k, v) in &game.env {
|
for (k, v) in &game.env {
|
||||||
cmd.env(k, v);
|
cmd.env(k, v);
|
||||||
}
|
}
|
||||||
let child = cmd.spawn().with_context(|| {
|
let mut child = cmd.spawn().with_context(|| {
|
||||||
format!("spawn {} {:?}", game.exe.display(), game.args)
|
format!("spawn {} {:?}", game.exe.display(), game.args)
|
||||||
})?;
|
})?;
|
||||||
let pid = child.id();
|
let pid = child.id();
|
||||||
@ -955,10 +975,15 @@ pub async fn launch_game(session: &Arc<Mutex<Session>>, name: &str) -> Result<()
|
|||||||
g.spawned_pids.insert(pid);
|
g.spawned_pids.insert(pid);
|
||||||
}
|
}
|
||||||
tracing::info!("launched game {name:?} as pid {pid}");
|
tracing::info!("launched game {name:?} as pid {pid}");
|
||||||
// Drop the Child: the OS keeps the process running. The
|
// Spawn a background thread to wait on the child. Without
|
||||||
// spawned_pids set is the only thing we need to remember; the
|
// this the child becomes a zombie (we are the parent; the
|
||||||
// kernel reaps the child when it exits.
|
// kernel keeps it around until wait). The child runs in
|
||||||
drop(child);
|
// parallel with the daemon; spawned_pids still records its
|
||||||
|
// PID. Wine wrappers often exit fast so this also avoids
|
||||||
|
// the zombie pile-up that Grok round 5 flagged.
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = child.wait();
|
||||||
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -968,33 +993,45 @@ pub async fn launch_game(session: &Arc<Mutex<Session>>, name: &str) -> Result<()
|
|||||||
/// future operator resize.
|
/// future operator resize.
|
||||||
pub async fn resize_slot(session: &Arc<Mutex<Session>>, slot: u32) -> Result<()> {
|
pub async fn resize_slot(session: &Arc<Mutex<Session>>, slot: u32) -> Result<()> {
|
||||||
use crate::profile::LayoutMode;
|
use crate::profile::LayoutMode;
|
||||||
let (slot_count, mode, slot_idx) = {
|
let (_mode, win_addr, w, h) = {
|
||||||
let g = session.lock().await;
|
let g = session.lock().await;
|
||||||
(g.slots.len(), g.engine.profile.layout.mode, slot.saturating_sub(1) as usize)
|
if g.engine.profile.layout.mode != LayoutMode::Free {
|
||||||
};
|
anyhow::bail!("resize-slot is only valid in Free layout mode");
|
||||||
if mode != LayoutMode::Free {
|
}
|
||||||
anyhow::bail!("resize-slot is only valid in Free layout mode");
|
// Round-5 Item 7: find the slot by ID, not by Vec index.
|
||||||
}
|
// Session.slots can have gaps (e.g. [(1,a),(3,b)] after a
|
||||||
if slot_idx >= slot_count {
|
// hide) so index-based lookup would mis-route resize-slot
|
||||||
anyhow::bail!("slot {slot} out of range (have {slot_count})");
|
// to the wrong layout slot. layout.slots is now keyed by
|
||||||
}
|
// slot_id via the LayoutSlot.id field added below.
|
||||||
let win_addr = {
|
let win_addr = g
|
||||||
let g = session.lock().await;
|
.slots
|
||||||
g.slots.iter()
|
.iter()
|
||||||
.find(|(s, _)| *s == slot)
|
.find(|(s, _)| *s == slot)
|
||||||
.map(|(_, c)| c.address.clone())
|
.map(|(_, c)| c.address.clone())
|
||||||
.ok_or_else(|| anyhow::anyhow!("no window for slot {slot}"))?
|
.ok_or_else(|| anyhow::anyhow!("no window for slot {slot}"))?;
|
||||||
};
|
let layout_slot = g
|
||||||
let (w, h) = {
|
.engine
|
||||||
let g = session.lock().await;
|
.profile
|
||||||
let s = g.engine.profile.layout.slots[slot_idx].clone();
|
.layout
|
||||||
s.initial_size.unwrap_or((s.w.max(0) as u32, s.h.max(0) as u32))
|
.slots
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.id == slot)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("no layout slot for slot id {slot}")
|
||||||
|
})?;
|
||||||
|
let (w, h) = layout_slot
|
||||||
|
.initial_size
|
||||||
|
.unwrap_or((layout_slot.w.max(0) as u32, layout_slot.h.max(0) as u32));
|
||||||
|
(g.engine.profile.layout.mode, win_addr, w, h)
|
||||||
};
|
};
|
||||||
let sel = format!("address:0x{win_addr}");
|
let sel = format!("address:0x{win_addr}");
|
||||||
crate::hypr::resize_window(&sel, w as i32, h as i32).await?;
|
crate::hypr::resize_window(&sel, w as i32, h as i32).await?;
|
||||||
{
|
{
|
||||||
let mut g = session.lock().await;
|
let mut g = session.lock().await;
|
||||||
g.engine.profile.layout.slots[slot_idx].size_locked = true;
|
if let Some(ls) = g.engine.profile.layout.slots.iter_mut().find(|s| s.id == slot) {
|
||||||
|
ls.size_locked = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -1003,27 +1040,30 @@ pub async fn resize_slot(session: &Arc<Mutex<Session>>, slot: u32) -> Result<()>
|
|||||||
/// to the slot's pos (or cached x/y).
|
/// to the slot's pos (or cached x/y).
|
||||||
pub async fn move_slot(session: &Arc<Mutex<Session>>, slot: u32) -> Result<()> {
|
pub async fn move_slot(session: &Arc<Mutex<Session>>, slot: u32) -> Result<()> {
|
||||||
use crate::profile::LayoutMode;
|
use crate::profile::LayoutMode;
|
||||||
let (slot_count, mode, slot_idx) = {
|
let (win_addr, x, y) = {
|
||||||
let g = session.lock().await;
|
let g = session.lock().await;
|
||||||
(g.slots.len(), g.engine.profile.layout.mode, slot.saturating_sub(1) as usize)
|
if g.engine.profile.layout.mode != LayoutMode::Free {
|
||||||
};
|
anyhow::bail!("move-slot is only valid in Free layout mode");
|
||||||
if mode != LayoutMode::Free {
|
}
|
||||||
anyhow::bail!("move-slot is only valid in Free layout mode");
|
let win_addr = g
|
||||||
}
|
.slots
|
||||||
if slot_idx >= slot_count {
|
.iter()
|
||||||
anyhow::bail!("slot {slot} out of range (have {slot_count})");
|
|
||||||
}
|
|
||||||
let win_addr = {
|
|
||||||
let g = session.lock().await;
|
|
||||||
g.slots.iter()
|
|
||||||
.find(|(s, _)| *s == slot)
|
.find(|(s, _)| *s == slot)
|
||||||
.map(|(_, c)| c.address.clone())
|
.map(|(_, c)| c.address.clone())
|
||||||
.ok_or_else(|| anyhow::anyhow!("no window for slot {slot}"))?
|
.ok_or_else(|| anyhow::anyhow!("no window for slot {slot}"))?;
|
||||||
};
|
let layout_slot = g
|
||||||
let (x, y) = {
|
.engine
|
||||||
let g = session.lock().await;
|
.profile
|
||||||
let s = g.engine.profile.layout.slots[slot_idx].clone();
|
.layout
|
||||||
s.pos.unwrap_or((s.x, s.y))
|
.slots
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.id == slot)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("no layout slot for slot id {slot}")
|
||||||
|
})?;
|
||||||
|
let (x, y) = layout_slot.pos.unwrap_or((layout_slot.x, layout_slot.y));
|
||||||
|
(win_addr, x, y)
|
||||||
};
|
};
|
||||||
let sel = format!("address:0x{win_addr}");
|
let sel = format!("address:0x{win_addr}");
|
||||||
crate::hypr::move_window(&sel, x, y).await?;
|
crate::hypr::move_window(&sel, x, y).await?;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user