diff --git a/src/dock.rs b/src/dock.rs index 044ef17..1e74b85 100644 --- a/src/dock.rs +++ b/src/dock.rs @@ -1,9 +1,12 @@ // Dockfönstren: ett layer-shell-fönster per skärm. Bygger ikonraden av -// pins + körande fönstergrupper och kopplar klick/skroll/menyer. +// pins + körande fönstergrupper, kopplar klick/skroll/menyer och sköter +// autohide (alltid-göm eller dodge via Wayfire-IPC-geometri). -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; +use std::collections::HashSet; use std::path::PathBuf; use std::rc::Rc; +use std::time::Duration; use gtk4 as gtk; use gtk4::gdk; @@ -13,7 +16,8 @@ use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; use log::{debug, info}; use crate::config::{ - ClickAction, Layer as CfgLayer, MiddleClick, Position, ScrollAction, SharedConfig, + AutohideMode, ClickAction, Layer as CfgLayer, MiddleClick, Position, ScrollAction, + SharedConfig, }; use crate::icons::{self, AppMeta}; use crate::menu; @@ -25,20 +29,35 @@ pub struct Ctx { pub cfg: SharedConfig, pub cfg_path: PathBuf, pub service: RefCell>>, - pub docks: RefCell>, + pub docks: RefCell>>, /// målet för den öppna kontextmenyn (menu.rs läser) pub menu_target: RefCell>, pub app: gtk::Application, /// körs när configen ändrats (sätts av main; applicerar CSS/rebuild) pub on_config_changed: RefCell>>, /// generationsräknare för settings-GUI:ts debouncade sparningar - pub save_generation: std::cell::Cell, + pub save_generation: Cell, + /// app-nycklar vars fönster kräver uppmärksamhet (via Wayfire-events) + pub urgent: RefCell>, + /// hot corner/edge-fönster (hotspots.rs äger innehållet) + pub hotspot_windows: RefCell>, } pub struct Dock { pub window: gtk::ApplicationWindow, pub panel: gtk::Box, + pub revealer: gtk::Revealer, pub connector: String, + /// pekaren är över dockan just nu + pub hover: Cell, + /// dockan är undan-gömd av autohide + pub hidden: Cell, + /// något fönster överlappar dockans kantzon (dodge) + pub dodge_overlap: Cell, + /// generationsräknare för fördröjd göm-timer + hide_generation: Cell, + /// sensorremsan som väcker dockan när den är gömd + sensor: RefCell>, } /// En ikon i docken: en pin och/eller en grupp körande fönster. @@ -55,6 +74,22 @@ fn orientation(pos: &Position) -> gtk::Orientation { } } +fn main_edge(pos: &Position) -> Edge { + match pos { + Position::Bottom => Edge::Bottom, + Position::Top => Edge::Top, + Position::Left => Edge::Left, + Position::Right => Edge::Right, + } +} + +fn side_edges(pos: &Position) -> (Edge, Edge) { + match pos { + Position::Bottom | Position::Top => (Edge::Left, Edge::Right), + _ => (Edge::Top, Edge::Bottom), + } +} + fn configure_layer(win: >k::ApplicationWindow, ctx: &Ctx, monitor: &gdk::Monitor) { let cfg = ctx.cfg.borrow(); win.init_layer_shell(); @@ -67,20 +102,15 @@ fn configure_layer(win: >k::ApplicationWindow, ctx: &Ctx, monitor: &gdk::Monit }); win.set_keyboard_mode(KeyboardMode::None); - let (main_edge, side_a, side_b) = match cfg.position { - Position::Bottom => (Edge::Bottom, Edge::Left, Edge::Right), - Position::Top => (Edge::Top, Edge::Left, Edge::Right), - Position::Left => (Edge::Left, Edge::Top, Edge::Bottom), - Position::Right => (Edge::Right, Edge::Top, Edge::Bottom), - }; - win.set_anchor(main_edge, true); - win.set_margin(main_edge, cfg.margin); + let edge = main_edge(&cfg.position); + let (side_a, side_b) = side_edges(&cfg.position); + win.set_anchor(edge, true); + win.set_margin(edge, cfg.margin); if !cfg.centered { - // full bredd/höjd längs kanten win.set_anchor(side_a, true); win.set_anchor(side_b, true); } - if cfg.avoid_windows { + if cfg.avoid_windows && !cfg.autohide.enabled { win.auto_exclusive_zone_enable(); } else { win.set_exclusive_zone(0); @@ -90,6 +120,9 @@ fn configure_layer(win: >k::ApplicationWindow, ctx: &Ctx, monitor: &gdk::Monit /// Bygg om alla dockfönster från grunden (vid start, configbyte, skärmbyte). pub fn rebuild_all(ctx: &Rc) { for dock in ctx.docks.borrow_mut().drain(..) { + if let Some(sensor) = dock.sensor.borrow_mut().take() { + sensor.destroy(); + } dock.window.destroy(); } @@ -113,9 +146,11 @@ pub fn rebuild_all(ctx: &Rc) { } *ctx.docks.borrow_mut() = docks; refresh_items(ctx); + recompute_dodge(ctx); + update_autohide(ctx); } -fn build_dock(ctx: &Rc, monitor: &gdk::Monitor, connector: String) -> Dock { +fn build_dock(ctx: &Rc, monitor: &gdk::Monitor, connector: String) -> Rc { let cfg = ctx.cfg.borrow(); let win = gtk::ApplicationWindow::new(&ctx.app); win.add_css_class("waydock"); @@ -138,12 +173,41 @@ fn build_dock(ctx: &Rc, monitor: &gdk::Monitor, connector: String) -> Dock outer.set_margin_start(pad); outer.set_margin_end(pad); outer.append(&panel); - win.set_child(Some(&outer)); + + // revealern ger autohide-animationen + let revealer = gtk::Revealer::new(); + revealer.set_transition_type(match cfg.position { + Position::Bottom => gtk::RevealerTransitionType::SlideUp, + Position::Top => gtk::RevealerTransitionType::SlideDown, + Position::Left => gtk::RevealerTransitionType::SlideRight, + Position::Right => gtk::RevealerTransitionType::SlideLeft, + }); + revealer.set_transition_duration(if cfg.appearance.animations { + cfg.autohide.animation_ms + } else { + 0 + }); + revealer.set_reveal_child(true); + revealer.set_child(Some(&outer)); + win.set_child(Some(&revealer)); + let autohide_enabled = cfg.autohide.enabled; drop(cfg); configure_layer(&win, ctx, monitor); menu::setup_dock_actions(ctx, &win); + let dock = Rc::new(Dock { + window: win.clone(), + panel: panel.clone(), + revealer, + connector, + hover: Cell::new(false), + hidden: Cell::new(false), + dodge_overlap: Cell::new(false), + hide_generation: Cell::new(0), + sensor: RefCell::new(None), + }); + // högerklick på tom panelyta → panelmenyn let empty_click = gtk::GestureClick::new(); empty_click.set_button(3); @@ -157,9 +221,198 @@ fn build_dock(ctx: &Rc, monitor: &gdk::Monitor, connector: String) -> Dock } panel.add_controller(empty_click); + // hover-spårning för autohide + let motion = gtk::EventControllerMotion::new(); + { + let dock2 = dock.clone(); + motion.connect_enter(move |_, _, _| { + dock2.hover.set(true); + dock2.hide_generation.set(dock2.hide_generation.get() + 1); + }); + } + { + let ctx = ctx.clone(); + let dock2 = dock.clone(); + motion.connect_leave(move |_| { + dock2.hover.set(false); + schedule_hide(&ctx, &dock2); + }); + } + win.add_controller(motion); + + if autohide_enabled { + build_sensor(ctx, &dock, monitor); + } + win.present(); - info!("dock uppe på {connector}"); - Dock { window: win, panel, connector } + info!("dock uppe på {}", dock.connector); + dock +} + +/// Den tunna remsan vid skärmkanten som väcker en gömd dock. +fn build_sensor(ctx: &Rc, dock: &Rc, monitor: &gdk::Monitor) { + let cfg = ctx.cfg.borrow(); + let sensor = gtk::ApplicationWindow::new(&ctx.app); + sensor.add_css_class("waydock-sensor"); + sensor.init_layer_shell(); + sensor.set_namespace(Some("waydock-sensor")); + sensor.set_monitor(Some(monitor)); + sensor.set_layer(Layer::Overlay); + sensor.set_keyboard_mode(KeyboardMode::None); + sensor.set_exclusive_zone(0); + + let edge = main_edge(&cfg.position); + let (side_a, side_b) = side_edges(&cfg.position); + sensor.set_anchor(edge, true); + sensor.set_anchor(side_a, true); + sensor.set_anchor(side_b, true); + + let strip = gtk::Box::new(gtk::Orientation::Horizontal, 0); + let px = cfg.autohide.reveal_px.max(1); + match cfg.position { + Position::Bottom | Position::Top => strip.set_size_request(-1, px), + _ => strip.set_size_request(px, -1), + } + drop(cfg); + sensor.set_child(Some(&strip)); + + let motion = gtk::EventControllerMotion::new(); + { + let ctx = ctx.clone(); + let dock2 = dock.clone(); + motion.connect_enter(move |_, _, _| { + if dock2.hidden.get() { + set_dock_hidden(&dock2, false); + // om pekaren aldrig når dockan: göm igen efter en stund + let ctx2 = ctx.clone(); + let dock3 = dock2.clone(); + glib::timeout_add_local_once(Duration::from_millis(1500), move || { + if !dock3.hover.get() { + update_autohide(&ctx2); + } + }); + } + }); + } + strip.add_controller(motion); + + sensor.present(); + *dock.sensor.borrow_mut() = Some(sensor); +} + +fn set_dock_hidden(dock: &Rc, hidden: bool) { + if dock.hidden.get() == hidden { + return; + } + dock.hidden.set(hidden); + if hidden { + let duration = dock.revealer.transition_duration(); + dock.revealer.set_reveal_child(false); + let dock2 = dock.clone(); + glib::timeout_add_local_once(Duration::from_millis(duration as u64 + 30), move || { + if dock2.hidden.get() { + dock2.window.set_visible(false); + } + }); + } else { + dock.window.set_visible(true); + dock.revealer.set_reveal_child(true); + } +} + +fn should_hide(ctx: &Ctx, dock: &Dock) -> bool { + let cfg = ctx.cfg.borrow(); + if !cfg.autohide.enabled || dock.hover.get() { + return false; + } + match cfg.autohide.mode { + AutohideMode::Always => true, + AutohideMode::Dodge => { + if wayfire_ipc::available() { + dock.dodge_overlap.get() + } else { + true // ingen geometri att gå på — bete dig som always + } + } + } +} + +/// Applicera göm/visa-läget på alla dockor. +pub fn update_autohide(ctx: &Rc) { + for dock in ctx.docks.borrow().iter() { + set_dock_hidden(dock, should_hide(ctx, dock)); + } +} + +fn schedule_hide(ctx: &Rc, dock: &Rc) { + let delay = ctx.cfg.borrow().autohide.hide_delay_ms; + let generation = dock.hide_generation.get() + 1; + dock.hide_generation.set(generation); + let ctx = ctx.clone(); + let dock = dock.clone(); + glib::timeout_add_local_once(Duration::from_millis(delay as u64), move || { + if dock.hide_generation.get() == generation && should_hide(&ctx, &dock) { + set_dock_hidden(&dock, true); + } + }); +} + +/// Räkna om dodge-överlapp för alla dockor via Wayfire-IPC-geometri. +pub fn recompute_dodge(ctx: &Rc) { + { + let cfg = ctx.cfg.borrow(); + if !cfg.autohide.enabled || cfg.autohide.mode != AutohideMode::Dodge { + return; + } + } + let Some(views) = wayfire_ipc::list_views() else { return }; + let Some(outputs) = wayfire_ipc::list_outputs() else { return }; + let position = ctx.cfg.borrow().position.clone(); + let margin = ctx.cfg.borrow().margin; + + for dock in ctx.docks.borrow().iter() { + let Some(output) = outputs + .iter() + .find(|o| o["name"].as_str() == Some(dock.connector.as_str())) + else { + continue; + }; + let ow = output["geometry"]["width"].as_f64().unwrap_or(0.0); + let oh = output["geometry"]["height"].as_f64().unwrap_or(0.0); + // zonen dockan upptar vid sin kant (fönstrets storlek + marginal) + let depth = match position { + Position::Bottom | Position::Top => dock.window.height().max(40) + margin, + _ => dock.window.width().max(40) + margin, + } as f64; + + let overlap = views.iter().any(|v| { + if v["role"].as_str() != Some("toplevel") + || !v["mapped"].as_bool().unwrap_or(false) + || v["minimized"].as_bool().unwrap_or(false) + || v["output-name"].as_str() != Some(dock.connector.as_str()) + { + return false; + } + let g = &v["geometry"]; + let (x, y) = (g["x"].as_f64().unwrap_or(0.0), g["y"].as_f64().unwrap_or(0.0)); + let (w, h) = ( + g["width"].as_f64().unwrap_or(0.0), + g["height"].as_f64().unwrap_or(0.0), + ); + // utanför aktuell arbetsyta → ingen kollision + if x + w <= 0.0 || y + h <= 0.0 || x >= ow || y >= oh { + return false; + } + match position { + Position::Bottom => y + h >= oh - depth, + Position::Top => y <= depth, + Position::Left => x <= depth, + Position::Right => x + w >= ow - depth, + } + }); + dock.dodge_overlap.set(overlap); + } + update_autohide(ctx); } /// Gruppera snapshot → ikonrad för en viss skärm. @@ -223,12 +476,8 @@ fn special_button(icon_name: &str, tooltip: &str) -> gtk::Button { btn } -fn add_special_buttons(ctx: &Rc, dock: &Dock) { - let cfg = ctx.cfg.borrow(); - let specials = cfg.special_buttons.clone(); - let icon_size = cfg.appearance.icon_size; - drop(cfg); - let _ = icon_size; +fn add_special_buttons(ctx: &Rc, dock: &Rc) { + let specials = ctx.cfg.borrow().special_buttons.clone(); if specials.overview && wayfire_ipc::available() { let btn = special_button("view-grid-symbolic", "Översikt (scale)"); @@ -252,12 +501,17 @@ fn build_item_widget(ctx: &Rc, item: Rc) -> gtk::Button { let icon_size = cfg.appearance.icon_size; let max_dots = cfg.appearance.indicator.max as usize; let tooltips = cfg.behavior.tooltips; + let urgent_pulse = cfg.behavior.urgent_pulse; drop(cfg); let btn = gtk::Button::new(); btn.add_css_class("dock-item"); btn.set_has_frame(false); + if urgent_pulse && ctx.urgent.borrow().contains(&item.meta.key) { + btn.add_css_class("urgent"); + } + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 2); let overlay = gtk::Overlay::new(); overlay.set_child(Some(&icons::image_for(&item.meta, icon_size))); diff --git a/src/hotspots.rs b/src/hotspots.rs new file mode 100644 index 0000000..8bba274 --- /dev/null +++ b/src/hotspots.rs @@ -0,0 +1,162 @@ +// Hot corners & hot edges: små osynliga layer-ytor vid skärmens hörn +// och kanter. Pekaren måste vila där i dwell_ms innan actionen körs +// (exec: eller ipc:), och måste lämna ytan innan den +// kan trigga igen. + +use std::cell::Cell; +use std::rc::Rc; +use std::time::Duration; + +use gtk4 as gtk; +use gtk4::gdk; +use gtk4::glib; +use gtk4::prelude::*; +use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; +use log::{info, warn}; + +use crate::config::HotspotAction; +use crate::dock::Ctx; +use crate::wayfire_ipc; + +const CORNER_PX: i32 = 6; +const EDGE_PX: i32 = 2; + +fn run_action(action: &HotspotAction) { + match action { + HotspotAction::Exec(cmd) => { + info!("hotspot: kör {cmd}"); + let cmd = cmd.clone(); + if let Err(e) = glib::spawn_command_line_async(&cmd) { + warn!("hotspot-kommando misslyckades: {e}"); + } + } + HotspotAction::Ipc(method) => { + info!("hotspot: ipc {method}"); + if let Err(e) = wayfire_ipc::call(method, serde_json::Value::Null) { + warn!("hotspot-ipc {method}: {e}"); + } + } + } +} + +/// En hotspot-yta: `anchors` anger vilka kanter den fästs mot +/// (två = hörn, en/tre = kant), `size` (w, h) där -1 = sträck ut. +fn build_hotspot( + ctx: &Rc, + monitor: &gdk::Monitor, + anchors: &[Edge], + size: (i32, i32), + action: HotspotAction, + dwell_ms: u32, +) -> gtk::ApplicationWindow { + let win = gtk::ApplicationWindow::new(&ctx.app); + win.add_css_class("waydock-hotspot"); + win.init_layer_shell(); + win.set_namespace(Some("waydock-hotspot")); + win.set_monitor(Some(monitor)); + win.set_layer(Layer::Overlay); + win.set_keyboard_mode(KeyboardMode::None); + win.set_exclusive_zone(0); + for edge in anchors { + win.set_anchor(*edge, true); + } + + let area = gtk::Box::new(gtk::Orientation::Horizontal, 0); + area.set_size_request(size.0, size.1); + win.set_child(Some(&area)); + + let armed = Rc::new(Cell::new(true)); + let inside = Rc::new(Cell::new(false)); + let motion = gtk::EventControllerMotion::new(); + { + let armed = armed.clone(); + let inside = inside.clone(); + motion.connect_enter(move |_, _, _| { + inside.set(true); + if !armed.get() { + return; + } + let armed = armed.clone(); + let inside = inside.clone(); + let action = action.clone(); + glib::timeout_add_local_once(Duration::from_millis(dwell_ms as u64), move || { + if inside.get() && armed.get() { + armed.set(false); // återarmeras när pekaren lämnar + run_action(&action); + } + }); + }); + } + { + let armed = armed.clone(); + let inside = inside.clone(); + motion.connect_leave(move |_| { + inside.set(false); + armed.set(true); + }); + } + area.add_controller(motion); + + win.present(); + win +} + +/// Bygg om alla hotspot-ytor enligt configen (körs vid start + configbyte). +pub fn rebuild(ctx: &Rc) { + for win in ctx.hotspot_windows.borrow_mut().drain(..) { + win.destroy(); + } + + let cfg = ctx.cfg.borrow(); + let dwell = cfg.hotspots.dwell_ms; + let c = cfg.hotspots.corners.clone(); + let e = cfg.hotspots.edges.clone(); + drop(cfg); + + let corner_defs: [(&Option, [Edge; 2]); 4] = [ + (&c.top_left, [Edge::Top, Edge::Left]), + (&c.top_right, [Edge::Top, Edge::Right]), + (&c.bottom_left, [Edge::Bottom, Edge::Left]), + (&c.bottom_right, [Edge::Bottom, Edge::Right]), + ]; + // kanterna lämnar hörnen fria (hörn-ytor ligger ovanpå ändå — overlay + // med senare mappade ytor hamnar överst, så det kvittar i praktiken) + #[allow(clippy::type_complexity)] + let edge_defs: [(&Option, [Edge; 3], (i32, i32)); 4] = [ + (&e.top, [Edge::Top, Edge::Left, Edge::Right], (-1, EDGE_PX)), + (&e.bottom, [Edge::Bottom, Edge::Left, Edge::Right], (-1, EDGE_PX)), + (&e.left, [Edge::Left, Edge::Top, Edge::Bottom], (EDGE_PX, -1)), + (&e.right, [Edge::Right, Edge::Top, Edge::Bottom], (EDGE_PX, -1)), + ]; + + let display = gdk::Display::default().expect("ingen display"); + let monitors = display.monitors(); + let mut windows = Vec::new(); + for i in 0..monitors.n_items() { + let Some(monitor) = monitors.item(i).and_then(|o| o.downcast::().ok()) + else { + continue; + }; + for (action, edges) in &corner_defs { + if let Some(a) = action { + windows.push(build_hotspot( + ctx, + &monitor, + edges, + (CORNER_PX, CORNER_PX), + a.clone(), + dwell, + )); + } + } + for (action, edges, size) in &edge_defs { + if let Some(a) = action { + windows.push(build_hotspot(ctx, &monitor, edges, *size, a.clone(), dwell)); + } + } + } + if !windows.is_empty() { + info!("{} hotspot-ytor aktiva", windows.len()); + } + *ctx.hotspot_windows.borrow_mut() = windows; +} diff --git a/src/main.rs b/src/main.rs index 896a5d9..453a585 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod config; mod dock; +mod hotspots; mod icons; mod menu; mod settings; @@ -110,7 +111,7 @@ fn layout_signature(cfg: &config::Config) -> String { cfg.appearance.shadow.enabled, cfg.appearance.shadow.blur, cfg.outputs.keys().collect::>(), - ) + ) + &format!("|{:?}", cfg.autohide) } fn main() -> glib::ExitCode { @@ -149,6 +150,8 @@ fn activate(app: >k::Application, cfg_path: PathBuf) { app: app.clone(), on_config_changed: RefCell::new(None), save_generation: Cell::new(0), + urgent: RefCell::new(Default::default()), + hotspot_windows: RefCell::new(Vec::new()), }); // CSS + rebuild/refresh-logik @@ -170,6 +173,7 @@ fn activate(app: >k::Application, cfg_path: PathBuf) { } else { dock::refresh_items(&ctx2); } + hotspots::rebuild(&ctx2); }); *ctx.on_config_changed.borrow_mut() = Some(apply); } @@ -186,8 +190,10 @@ fn activate(app: >k::Application, cfg_path: PathBuf) { } } - if !wayfire_ipc::available() { - warn!("WAYFIRE_SOCKET saknas — översikt/flytta-till-skärm/dodge avstängda (aktivera ipc-pluginen i wayfire.ini)"); + if wayfire_ipc::available() { + subscribe_wayfire_events(&ctx); + } else { + warn!("Wayfire-IPC saknas — översikt/flytta-till-skärm/dodge avstängda (aktivera ipc-pluginen i wayfire.ini)"); } // första uppritningen @@ -230,3 +236,66 @@ fn activate(app: >k::Application, cfg_path: PathBuf) { }); } } + +/// Händelsestyrd dodge + urgency via Wayfires eventström. +fn subscribe_wayfire_events(ctx: &Rc) { + let ctx2 = ctx.clone(); + let pending = Rc::new(Cell::new(0u64)); + let result = wayfire_ipc::subscribe(Rc::new(move |ev| { + let name = ev["event"].as_str().unwrap_or(""); + match name { + "view-hints-changed" => { + let view = &ev["view"]; + let app_id = view["app-id"].as_str().unwrap_or(""); + if app_id.is_empty() { + return; + } + let key = icons::lookup(app_id).key; + let wants = ev["hints"]["demands-attention"].as_bool() + .or_else(|| view["demands-attention"].as_bool()) + .unwrap_or(false); + let changed = if wants { + ctx2.urgent.borrow_mut().insert(key) + } else { + ctx2.urgent.borrow_mut().remove(&key) + }; + if changed { + dock::refresh_items(&ctx2); + } + } + "view-focused" => { + let app_id = ev["view"]["app-id"].as_str().unwrap_or(""); + if !app_id.is_empty() { + let key = icons::lookup(app_id).key; + if ctx2.urgent.borrow_mut().remove(&key) { + dock::refresh_items(&ctx2); + } + } + schedule_dodge(&ctx2, &pending); + } + n if n.starts_with("view-") || n.starts_with("output-") => { + schedule_dodge(&ctx2, &pending); + } + _ => {} + } + })); + if let Err(e) = result { + warn!("kunde inte prenumerera på wayfire-events: {e}"); + } +} + +/// Debounca dodge-omräkningen (events kommer i skurar). +fn schedule_dodge(ctx: &Rc, pending: &Rc>) { + if !ctx.cfg.borrow().autohide.enabled { + return; + } + let generation = pending.get() + 1; + pending.set(generation); + let ctx = ctx.clone(); + let pending = pending.clone(); + glib::timeout_add_local_once(Duration::from_millis(120), move || { + if pending.get() == generation { + dock::recompute_dodge(&ctx); + } + }); +} diff --git a/src/style.rs b/src/style.rs index 014628b..dabae9c 100644 --- a/src/style.rs +++ b/src/style.rs @@ -52,10 +52,17 @@ pub fn build_css(cfg: &Config) -> String { format!( r#" -window.waydock {{ +window.waydock, +window.waydock-sensor, +window.waydock-hotspot {{ background: transparent; }} +window.waydock-sensor box, +window.waydock-hotspot box {{ + background-color: rgba(0, 0, 0, 0.004); +}} + .dock-panel {{ background-color: {bg}; border-radius: {radius}px; diff --git a/src/wayfire_ipc.rs b/src/wayfire_ipc.rs index b9f3b8c..3781ffc 100644 --- a/src/wayfire_ipc.rs +++ b/src/wayfire_ipc.rs @@ -15,11 +15,22 @@ pub fn socket_path() -> Option { if let Ok(p) = std::env::var("WAYFIRE_SOCKET") { return Some(p); } - // ipc-pluginen aktiverad efter sessionsstart → env saknas men - // socketen ligger på standardvägen /tmp/wayfire-.socket - let display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-1".into()); - let fallback = format!("/tmp/wayfire-{display}.socket"); - std::path::Path::new(&fallback).exists().then_some(fallback) + // ipc-pluginen aktiverad efter sessionsstart → env saknas; leta upp + // socketen (t.ex. $XDG_RUNTIME_DIR/wayfire-wayland-1-.socket) + let mut dirs = vec!["/tmp".to_string()]; + if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") { + dirs.insert(0, runtime); + } + for dir in dirs { + let Ok(entries) = std::fs::read_dir(&dir) else { continue }; + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with("wayfire-") && name.ends_with(".socket") { + return Some(entry.path().to_string_lossy().to_string()); + } + } + } + None } pub fn available() -> bool { @@ -108,6 +119,65 @@ pub fn find_view_id(app_id: &str, title: &str) -> Option { matches.first().and_then(|v| v["id"].as_i64()) } +/// Prenumerera på Wayfires händelseström (view-mapped, view-geometry-changed, +/// view-hints-changed …). Callbacken körs på GTK-tråden för varje event. +/// Strömmen lever så länge processen; returnerar Err om IPC saknas. +pub fn subscribe(on_event: std::rc::Rc) -> Result<(), String> { + use std::os::fd::AsRawFd; + + let path = socket_path().ok_or("WAYFIRE_SOCKET saknas")?; + let mut stream = UnixStream::connect(&path).map_err(|e| format!("connect: {e}"))?; + + // registrera prenumerationen synkront (svaret är {"result":"ok"}) + let payload = serde_json::to_vec(&json!({ "method": "window-rules/events/watch" })) + .map_err(|e| e.to_string())?; + let len = (payload.len() as u32).to_le_bytes(); + stream.write_all(&len).map_err(|e| format!("write: {e}"))?; + stream.write_all(&payload).map_err(|e| format!("write: {e}"))?; + stream + .set_nonblocking(true) + .map_err(|e| format!("nonblocking: {e}"))?; + + let fd = stream.as_raw_fd(); + let mut buf: Vec = Vec::new(); + let mut first = true; + gtk4::glib::source::unix_fd_add_local(fd, gtk4::glib::IOCondition::IN, move |_, _| { + // stream ägs av closuren — håll den vid liv + let mut chunk = [0u8; 8192]; + loop { + match (&stream).read(&mut chunk) { + Ok(0) => { + warn!("wayfire-ipc: händelseströmmen stängdes"); + return gtk4::glib::ControlFlow::Break; + } + Ok(n) => buf.extend_from_slice(&chunk[..n]), + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break, + Err(e) => { + warn!("wayfire-ipc: läsfel {e}"); + return gtk4::glib::ControlFlow::Break; + } + } + } + // plocka kompletta ramar ur bufferten + while buf.len() >= 4 { + let n = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize; + if buf.len() < 4 + n { + break; + } + let frame: Vec = buf.drain(0..4 + n).skip(4).collect(); + if let Ok(v) = serde_json::from_slice::(&frame) { + if first { + first = false; // svaret på watch-anropet + } else { + on_event(&v); + } + } + } + gtk4::glib::ControlFlow::Continue + }); + Ok(()) +} + /// Flytta en vy till en annan output (namn t.ex. "DP-1"). pub fn move_view_to_output(view_id: i64, output_name: &str) { let Some(outputs) = list_outputs() else {