feat(dock): add Hyprland-only macOS-style dock with auto-hiding - #1
feat(dock): add Hyprland-only macOS-style dock with auto-hiding#17n1m4 wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new Hyprland-only dock mode for vibepanel, featuring a centered bottom pill with pinned launchers, running window indicators, and an auto-hide mechanism. Feedback on the implementation highlights several key improvements: optimizing the dock window layout by avoiding full-width anchoring (which blocks mouse input) and letting GTK auto-size the width; correctly parsing and registering the quick settings popover; removing unsafe and unnecessary Send/Sync implementations on DockAutoHideState; optimizing the focus_window IPC command in the Hyprland backend; removing a duplicate warning check in the configuration validation; cleaning up an unused field in the launcher widget; and preventing zombie process leaks by asynchronously reaping spawned launcher processes.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // Anchor to the bottom edge and stretch across the full width. | ||
| window.set_anchor(Edge::Top, false); | ||
| window.set_anchor(Edge::Bottom, true); | ||
| window.set_anchor(Edge::Left, true); | ||
| window.set_anchor(Edge::Right, true); |
There was a problem hiding this comment.
Anchoring the dock window to both Left and Right makes it stretch across the entire width of the screen. Even though the window background is transparent, it will still intercept mouse clicks and block input to any underlying windows at the bottom of the screen.\n\nTo fix this, do not anchor the window to Left and Right. By only anchoring to Bottom, the compositor will automatically center the window horizontally, and its width will naturally shrink to fit the dock content. This completely eliminates the input blocking issue and makes the auto-hide behavior more precise.
| // Anchor to the bottom edge and stretch across the full width. | |
| window.set_anchor(Edge::Top, false); | |
| window.set_anchor(Edge::Bottom, true); | |
| window.set_anchor(Edge::Left, true); | |
| window.set_anchor(Edge::Right, true); | |
| // Anchor to the bottom edge and center horizontally (do not anchor Left/Right).\n window.set_anchor(Edge::Top, false);\n window.set_anchor(Edge::Bottom, true);\n window.set_anchor(Edge::Left, false);\n window.set_anchor(Edge::Right, false); |
| window.connect_map(move |win| { | ||
| win.set_default_size(target_width, dock_height as i32); | ||
| debug!( | ||
| "Set dock window size to target monitor width: {}px", | ||
| target_width | ||
| ); | ||
| }); |
There was a problem hiding this comment.
When centering the dock window horizontally (by not anchoring to Left and Right), we should not force the window width to target_width. Instead, we should let GTK auto-size the width to fit the content naturally, while only enforcing the height.
window.connect_map(move |win| {\n win.set_default_size(-1, dock_height as i32);\n debug!(\n \"Set dock window height: {}px\",\n dock_height\n );\n });| let qs = crate::widgets::QuickSettingsWindowHandle::new( | ||
| app.clone(), | ||
| crate::widgets::QuickSettingsConfig::default(), | ||
| ); |
There was a problem hiding this comment.
Creating the QuickSettingsWindowHandle with QuickSettingsConfig::default() ignores the user's custom quick settings configuration (such as custom shutdown/reboot commands). Additionally, the handle is not registered with the popover registry, which breaks IPC toggling and popover tracking.\n\nWe should parse the quick settings configuration from the user's config and register the handle with the popover registry, matching the behavior in bar.rs.
let qs_config = config\n .widgets\n .get_options(\"quick_settings\")\n .map(|opts| {\n let entry = vibepanel_core::config::WidgetEntry::with_options(\"quick_settings\", opts);\n crate::widgets::QuickSettingsConfig::from_entry(&entry)\n })\n .unwrap_or_default();\n let qs = crate::widgets::QuickSettingsWindowHandle::new(app.clone(), qs_config);\n crate::popover_registry::register(\n \"quick_settings\",\n Rc::new(qs.clone()) as Rc<dyn crate::popover_registry::PopoverToggleable>,\n );| fn focus_window(&self, window_id: u64) { | ||
| if let Some(clients) = self.query_json("clients") | ||
| && let Some(addr) = clients.as_array().and_then(|arr| { | ||
| arr.iter() | ||
| .find(|c| { | ||
| c.get("address").and_then(|a| a.as_str()).and_then(|a| { | ||
| u64::from_str_radix(a.strip_prefix("0x").unwrap_or(a), 16).ok() | ||
| }) == Some(window_id) | ||
| }) | ||
| .and_then(|c| c.get("address").and_then(|a| a.as_str().map(String::from))) | ||
| }) | ||
| { | ||
| let _ = self.send_command(&format!("dispatch focuswindow address:{addr}")); | ||
| } | ||
| } |
There was a problem hiding this comment.
This implementation of focus_window is highly inefficient. It queries all clients from Hyprland via IPC and parses a potentially large JSON array just to find the hex address string matching window_id.\n\nSince window_id is already the parsed u64 representation of the hex address, we can directly format it back to hex using 0x{:x} and send the command immediately. This completely avoids the IPC query and JSON parsing overhead.
fn focus_window(&self, window_id: u64) {\n let _ = self.send_command(&format!(\"dispatch focuswindow address:0x{:x}\", window_id));\n }| // Warn if dock mode requested without Hyprland. | ||
| if self.bar.mode == "dock" && self.advanced.compositor != "hyprland" { | ||
| warnings.push( | ||
| "bar.mode = \"dock\" is only supported on Hyprland; the dock will fall back to a bottom bar.".to_string(), | ||
| ); | ||
| } |
There was a problem hiding this comment.
| // Ensure the handle is Send + Sync for BarState's Vec<Box<dyn Any>>. | ||
| unsafe impl Send for DockAutoHideState {} | ||
| unsafe impl Sync for DockAutoHideState {} |
There was a problem hiding this comment.
Implementing Send and Sync unsafely on DockAutoHideState is dangerous and unnecessary. DockAutoHideState contains Rc<Cell<Option<SourceId>>>, which are fundamentally thread-unsafe (!Send and !Sync). Since BarState::add_handle accepts Box<dyn Any> without requiring Send or Sync bounds, these unsafe implementations can be safely removed to preserve Rust's safety guarantees.
| #[allow(dead_code)] | ||
| current_snapshot: Rc<RefCell<WindowListSnapshot>>, |
There was a problem hiding this comment.
|
|
||
| Self { | ||
| base, | ||
| current_snapshot: snapshot, |
| if let Err(e) = std::process::Command::new("sh").arg("-c").arg(exec).spawn() { | ||
| warn!("launcher: failed to spawn {:?}: {e}", exec); | ||
| } |
There was a problem hiding this comment.
Spawning a child process using std::process::Command::spawn without calling wait() or reaping it will leak zombie processes when the spawned application exits.\n\nTo prevent zombie processes, we should spawn a helper thread to wait on the child process asynchronously.
match std::process::Command::new(\"sh\").arg(\"-c\").arg(exec).spawn() {\n Ok(mut child) => {\n std::thread::spawn(move || {\n let _ = child.wait();\n });\n }\n Err(e) => {\n warn!(\"launcher: failed to spawn {:?}: {e}\", exec);\n }\n }cargo fmt --check in the pre-commit hook flagged the crate-local imports placed before the external ones. Reorder so all `crate::` imports follow the external crate imports, matching convention. Co-Authored-By: Claude (Opus 4.6) <<EMAIL>>
Summary
Adds a new
bar.mode = "dock"that transforms the vibepanel instance into a centered bottom "pill" (Dash-to-Dock style) showing pinned launchers and running-window indicators. The dock auto-hides when the mouse leaves and reveals when the cursor touches the bottom screen edge.Hyprland-only: falls back to a bottom bar on other compositors.
bar.mode+DockConfig(autohide,icon_size,launchers,pin_to_edge,background_opacity,gap,magnification,magnified_icon_size) with validation, warnings, and hot-reload awareness--color-accent/--dock-gaptheme-var registrationsrc/dock.rs) — bottom-anchored layer-shell surface with a thin 3px hotzone and opacity timer for reveal/hidesrc/widgets/launcher.rs) — pinned-app buttons ([widgets.launcher]TOML) that focus a running window or spawn viash -c, with running-indicator dots powered byWindowListServiceHyprlandBackend::list_windows()/set_window_list_callback()/focus_window()via theclientsJSON IPC query and event-driven snapshots, closing the long-standing Hyprland window-list gapVerification
cargo test --workspace: 642 + 151 + 27 + 1 doctest)cargo clippy --all -- -D warningscleancargo fmt --checkcleanTest plan
bar.mode = "dock"and confirm the centered bottom pill rendersexecon click when no matching window is openbar.modebetweenbaranddockhot-reloads without restart🤖 Generated with Claude Code