diff --git a/assets/waydock.desktop b/assets/waydock.desktop
new file mode 100644
index 0000000..1ce67bf
--- /dev/null
+++ b/assets/waydock.desktop
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Type=Application
+Name=waydock
+Comment=Dock för Wayfire — transparent, JSON-konfigurerad, per skärm
+Exec=waydock
+Icon=waydock
+Terminal=false
+Categories=Utility;
+Keywords=dock;panel;wayfire;taskbar;
+X-GNOME-Autostart-enabled=true
diff --git a/assets/waydock.svg b/assets/waydock.svg
new file mode 100644
index 0000000..380c8f5
--- /dev/null
+++ b/assets/waydock.svg
@@ -0,0 +1,36 @@
+
+
diff --git a/doc/usage.md b/doc/usage.md
index 26d88ae..427069d 100644
--- a/doc/usage.md
+++ b/doc/usage.md
@@ -23,7 +23,12 @@ Alla ändringar i filen slår igenom **live** — ingen omstart behövs.
- **Högerklick på tom yta**: Inställningar…, Ladda om config, Avsluta
- **Visa skrivbord-knappen** minimerar allt; klick igen återställer
- **Översikt-knappen** togglar Wayfires scale-vy (kräver ipc-pluginen)
-- Grupp-ikoner får en siffra och accentprickar; hovra för fönsterlistan
+- Grupp-ikoner får en siffra och accentprickar. Fönsterlistan öppnas
+ vid klick eller hover (`behavior.group_popup`); hovra en rad i listan
+ för peek (bara det fönstret syns, andra tonas ner — kräver Wayfires
+ alpha-plugin) eller fokus, enligt `behavior.list_hover`
+- Marginalillustrationen i Inställningar → Layout visar vad A/B/C-måtten
+ styr för aktuell position
## Settings-GUI
@@ -35,8 +40,11 @@ JSON-filen och appliceras live. GUI:t och handredigering kan blandas fritt.
```jsonc
{
"position": "bottom", // bottom | top | left | right
- "centered": true, // ö i mitten i stället för hela kanten
- "margin": 8, // px från skärmkant (0 = kant i kant)
+ "alignment": "center", // center | start | end — var längs kanten ön ligger
+ "expand": false, // true = täck hela kanten (panelen fyller bredden/höjden)
+ "margin": 8, // A: px från kanten baren ligger mot
+ "margin_start": 0, // B: marginal i början av kanten (vänster/topp)
+ "margin_end": 0, // C: marginal i slutet av kanten (höger/botten)
"layer": "top", // top | overlay | bottom (always-on-top = overlay)
"avoid_windows": false, // true = reservera plats (exclusive zone)
@@ -61,7 +69,10 @@ JSON-filen och appliceras live. GUI:t och handredigering kan blandas fritt.
"scroll": "cycle_windows", // cycle_windows | none
"group_apps": true,
"tooltips": true,
- "urgent_pulse": true
+ "urgent_pulse": true,
+ "group_popup": "click", // click | hover — hur fönsterlistan öppnas
+ "list_hover": "peek" // peek (visa bara det fönstret, tona ner andra)
+ // | focus (fokusera vid hover) | none
},
"context_menu": ["minimize", "maximize", "move_to_output",
diff --git a/install.sh b/install.sh
index 0056674..8dbd2df 100644
--- a/install.sh
+++ b/install.sh
@@ -17,6 +17,13 @@ curl -fSL -o "$DEST/waydock" \
"$BASE/releases/download/latest/waydock-latest-linux-x64"
chmod +x "$DEST/waydock"
+# ikon + .desktop-fil
+mkdir -p "$HOME/.local/share/icons/hicolor/scalable/apps" "$HOME/.local/share/applications"
+curl -fsSL -o "$HOME/.local/share/icons/hicolor/scalable/apps/waydock.svg" \
+ "$BASE/raw/branch/master/assets/waydock.svg" || true
+curl -fsSL -o "$HOME/.local/share/applications/waydock.desktop" \
+ "$BASE/raw/branch/master/assets/waydock.desktop" || true
+
echo "Installerad: $DEST/waydock"
case ":$PATH:" in
*":$DEST:"*) ;;
diff --git a/src/config.rs b/src/config.rs
index 90474ea..0797d81 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -28,6 +28,34 @@ pub enum Layer {
Bottom,
}
+/// Var längs kanten ön ligger (start = vänster/topp, end = höger/botten).
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+#[serde(rename_all = "snake_case")]
+pub enum Alignment {
+ Center,
+ Start,
+ End,
+}
+
+/// Hur fönsterlistan för en grupperad app öppnas.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+#[serde(rename_all = "snake_case")]
+pub enum GroupPopup {
+ Click,
+ Hover,
+}
+
+/// Vad som händer när man hovrar en rad i fönsterlistan.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+#[serde(rename_all = "snake_case")]
+pub enum ListHover {
+ /// tona ner alla andra fönster så bara det hovrade syns tydligt
+ Peek,
+ /// fokusera/lyft fönstret direkt vid hover
+ Focus,
+ None,
+}
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ClickAction {
@@ -151,6 +179,8 @@ pub struct Behavior {
pub group_apps: bool,
pub tooltips: bool,
pub urgent_pulse: bool,
+ pub group_popup: GroupPopup,
+ pub list_hover: ListHover,
}
impl Default for Behavior {
fn default() -> Self {
@@ -161,6 +191,8 @@ impl Default for Behavior {
group_apps: true,
tooltips: true,
urgent_pulse: true,
+ group_popup: GroupPopup::Click,
+ list_hover: ListHover::Peek,
}
}
}
@@ -268,8 +300,15 @@ impl Default for LogConfig {
#[serde(default)]
pub struct Config {
pub position: Position,
- pub centered: bool,
+ pub alignment: Alignment,
+ /// false = ö som är så stor som innehållet; true = täck hela kanten
+ pub expand: bool,
+ /// avstånd från kanten baren ligger mot (px)
pub margin: i32,
+ /// marginal i början av kanten: vänster (topp/botten) resp. topp (vänster/höger)
+ pub margin_start: i32,
+ /// marginal i slutet av kanten: höger resp. botten
+ pub margin_end: i32,
pub layer: Layer,
pub avoid_windows: bool,
pub appearance: Appearance,
@@ -287,8 +326,11 @@ impl Default for Config {
fn default() -> Self {
Self {
position: Position::Bottom,
- centered: true,
+ alignment: Alignment::Center,
+ expand: false,
margin: 8,
+ margin_start: 0,
+ margin_end: 0,
layer: Layer::Top,
avoid_windows: false,
appearance: Appearance::default(),
diff --git a/src/dock.rs b/src/dock.rs
index e8aa25d..d5d218a 100644
--- a/src/dock.rs
+++ b/src/dock.rs
@@ -16,8 +16,8 @@ use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
use log::{debug, info};
use crate::config::{
- AutohideMode, ClickAction, Layer as CfgLayer, MiddleClick, Position, ScrollAction,
- SharedConfig,
+ Alignment, AutohideMode, ClickAction, GroupPopup, Layer as CfgLayer, MiddleClick,
+ Position, ScrollAction, SharedConfig,
};
use crate::icons::{self, AppMeta};
use crate::menu;
@@ -45,7 +45,7 @@ pub struct Ctx {
pub struct Dock {
pub window: gtk::ApplicationWindow,
- pub panel: gtk::Box,
+ pub items_box: gtk::Box,
pub revealer: gtk::Revealer,
pub connector: String,
/// pekaren är över dockan just nu
@@ -103,12 +103,28 @@ fn configure_layer(win: >k::ApplicationWindow, ctx: &Ctx, monitor: &gdk::Monit
win.set_keyboard_mode(KeyboardMode::None);
let edge = main_edge(&cfg.position);
- let (side_a, side_b) = side_edges(&cfg.position);
+ let (side_start, side_end) = side_edges(&cfg.position);
win.set_anchor(edge, true);
win.set_margin(edge, cfg.margin);
- if !cfg.centered {
- win.set_anchor(side_a, true);
- win.set_anchor(side_b, true);
+ if cfg.expand {
+ // täck hela kanten; start/slut-marginalerna drar in baren
+ win.set_anchor(side_start, true);
+ win.set_anchor(side_end, true);
+ win.set_margin(side_start, cfg.margin_start);
+ win.set_margin(side_end, cfg.margin_end);
+ } else {
+ // ö: förankra bara i vald ände (ingen sidoförankring = centrerad)
+ match cfg.alignment {
+ Alignment::Center => {}
+ Alignment::Start => {
+ win.set_anchor(side_start, true);
+ win.set_margin(side_start, cfg.margin_start);
+ }
+ Alignment::End => {
+ win.set_anchor(side_end, true);
+ win.set_margin(side_end, cfg.margin_end);
+ }
+ }
}
if cfg.avoid_windows && !cfg.autohide.enabled {
win.auto_exclusive_zone_enable();
@@ -158,8 +174,30 @@ fn build_dock(ctx: &Rc, monitor: &gdk::Monitor, connector: String) -> Rc gtk::Align::Center,
+ Alignment::Start => gtk::Align::Start,
+ Alignment::End => gtk::Align::End,
+ };
+ let items_box = gtk::Box::new(orient, 0);
+ if cfg.expand {
+ // panelbakgrunden täcker kanten; ikonerna läggs enligt alignment
+ panel.set_halign(gtk::Align::Fill);
+ panel.set_valign(gtk::Align::Fill);
+ if orient == gtk::Orientation::Horizontal {
+ panel.set_hexpand(true);
+ items_box.set_hexpand(true);
+ items_box.set_halign(items_align);
+ } else {
+ panel.set_vexpand(true);
+ items_box.set_vexpand(true);
+ items_box.set_valign(items_align);
+ }
+ } else {
+ panel.set_halign(gtk::Align::Center);
+ panel.set_valign(gtk::Align::Center);
+ }
+ panel.append(&items_box);
// yttre marginal = plats för skuggan att rita
let outer = gtk::Box::new(orient, 0);
@@ -198,7 +236,7 @@ fn build_dock(ctx: &Rc, monitor: &gdk::Monitor, connector: String) -> Rc) {
.unwrap_or_default();
for dock in ctx.docks.borrow().iter() {
- while let Some(child) = dock.panel.first_child() {
- dock.panel.remove(&child);
+ while let Some(child) = dock.items_box.first_child() {
+ dock.items_box.remove(&child);
}
add_special_buttons(ctx, dock);
for item in build_items(ctx, &dock.connector, &snapshot) {
let widget = build_item_widget(ctx, &dock.connector, Rc::new(item));
- dock.panel.append(&widget);
+ dock.items_box.append(&widget);
}
}
}
@@ -509,7 +547,7 @@ fn add_special_buttons(ctx: &Rc, dock: &Rc) {
if specials.overview && wayfire_ipc::available() {
let btn = special_button("view-grid-symbolic", "Översikt (scale)");
btn.connect_clicked(|_| wayfire_ipc::toggle_scale());
- dock.panel.append(&btn);
+ dock.items_box.append(&btn);
}
if specials.show_desktop {
let btn = special_button("user-desktop-symbolic", "Visa skrivbordet");
@@ -519,7 +557,7 @@ fn add_special_buttons(ctx: &Rc, dock: &Rc) {
service.toggle_show_desktop();
}
});
- dock.panel.append(&btn);
+ dock.items_box.append(&btn);
}
}
@@ -610,6 +648,33 @@ fn build_item_widget(ctx: &Rc, connector: &str, item: Rc- ) -> gtk::But
}
btn.add_controller(click);
+ // hover-popup för grupperade appar (om valt i configen)
+ if item.windows.len() > 1
+ && ctx.cfg.borrow().behavior.group_popup == GroupPopup::Hover
+ {
+ let hover_ctl = gtk::EventControllerMotion::new();
+ let ctx2 = ctx.clone();
+ let item2 = item.clone();
+ let btn2 = btn.clone();
+ let conn2 = connector.to_string();
+ hover_ctl.connect_enter(move |ctl, _, _| {
+ let ctx3 = ctx2.clone();
+ let item3 = item2.clone();
+ let btn3 = btn2.clone();
+ let conn3 = conn2.clone();
+ let ctl2 = ctl.clone();
+ glib::timeout_add_local_once(Duration::from_millis(350), move || {
+ if ctl2.contains_pointer() {
+ let live = live_item(&ctx3, &conn3, &item3);
+ if live.windows.len() > 1 {
+ menu::popup_group_list(&ctx3, &live, &btn3);
+ }
+ }
+ });
+ });
+ btn.add_controller(hover_ctl);
+ }
+
// skroll: växla mellan gruppens fönster
let scroll = gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::VERTICAL);
{
@@ -649,8 +714,11 @@ fn primary_action(ctx: &Rc, item: &Rc
- , anchor: >k::Button) {
}
}
_ => {
- let click = ctx.cfg.borrow().behavior.click.clone();
- if click == ClickAction::Cycle {
+ let (click, popup) = {
+ let b = &ctx.cfg.borrow().behavior;
+ (b.click.clone(), b.group_popup.clone())
+ };
+ if click == ClickAction::Cycle || popup == GroupPopup::Hover {
cycle_windows(ctx, item, true);
} else {
menu::popup_group_list(ctx, item, anchor);
diff --git a/src/main.rs b/src/main.rs
index 453a585..c9421d1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -102,10 +102,13 @@ fn init_logging(cli: &Cli, cfg: &config::Config) {
/// Delar av configen som kräver att fönstren byggs om (inte bara CSS).
fn layout_signature(cfg: &config::Config) -> String {
format!(
- "{:?}|{}|{}|{:?}|{}|{}|{}|{:?}",
+ "{:?}|{:?}|{}|{}|{}|{}|{:?}|{}|{}|{}|{:?}",
cfg.position,
- cfg.centered,
+ cfg.alignment,
+ cfg.expand,
cfg.margin,
+ cfg.margin_start,
+ cfg.margin_end,
cfg.layer,
cfg.avoid_windows,
cfg.appearance.shadow.enabled,
diff --git a/src/menu.rs b/src/menu.rs
index 3ff8a93..bcbaf87 100644
--- a/src/menu.rs
+++ b/src/menu.rs
@@ -11,7 +11,7 @@ use gtk4::glib;
use gtk4::prelude::*;
use log::warn;
-use crate::config::{self, MenuItem};
+use crate::config::{self, GroupPopup, ListHover, MenuItem};
use crate::dock::{Ctx, Item};
use crate::icons;
use crate::settings;
@@ -232,12 +232,56 @@ pub fn popup_item_menu(ctx: &Rc, item: &Rc
- , anchor: >k::Button) {
popover.popup();
}
-/// Vänsterklick på grupp med flera fönster: lista fönstren.
+/// Tona ner alla andra fönster än det hovrade (peek). Returnerar id:n
+/// som dimmats så de kan återställas.
+fn peek_apply(hovered: &crate::toplevel::ToplevelInfo) -> Vec {
+ let Some(views) = wayfire_ipc::list_views() else {
+ return Vec::new();
+ };
+ let hovered_id = views
+ .iter()
+ .find(|v| {
+ v["app-id"].as_str() == Some(hovered.app_id.as_str())
+ && v["title"].as_str() == Some(hovered.title.as_str())
+ })
+ .and_then(|v| v["id"].as_i64());
+ let mut dimmed = Vec::new();
+ for v in &views {
+ if v["role"].as_str() != Some("toplevel")
+ || !v["mapped"].as_bool().unwrap_or(false)
+ || v["minimized"].as_bool().unwrap_or(false)
+ {
+ continue;
+ }
+ let Some(id) = v["id"].as_i64() else { continue };
+ if Some(id) != hovered_id {
+ wayfire_ipc::set_view_alpha(id, 0.15);
+ dimmed.push(id);
+ }
+ }
+ dimmed
+}
+
+fn peek_restore(dimmed: &[i64]) {
+ for id in dimmed {
+ wayfire_ipc::set_view_alpha(*id, 1.0);
+ }
+}
+
+/// Vänsterklick (eller hover) på grupp med flera fönster: lista fönstren.
pub fn popup_group_list(ctx: &Rc, item: &Rc
- , anchor: >k::Button) {
let popover = gtk::Popover::new();
popover.add_css_class("waydock-menu");
let list = gtk::Box::new(gtk::Orientation::Vertical, 2);
+ let (list_hover, hover_mode) = {
+ let b = &ctx.cfg.borrow().behavior;
+ (b.list_hover.clone(), b.group_popup == GroupPopup::Hover)
+ };
+ let peek_possible = list_hover == ListHover::Peek && wayfire_ipc::available();
+ // fönster som tonats ner av peek — återställs vid radbyte/stängning
+ let dimmed: Rc>> = Rc::new(std::cell::RefCell::new(Vec::new()));
+
for w in &item.windows {
let row = gtk::Button::new();
row.add_css_class("flat");
@@ -250,16 +294,76 @@ pub fn popup_group_list(ctx: &Rc, item: &Rc
- , anchor: >k::Button) {
let ctx2 = ctx.clone();
let w2 = w.clone();
let pop2 = popover.clone();
+ let dimmed2 = dimmed.clone();
row.connect_clicked(move |_| {
+ peek_restore(&dimmed2.borrow());
+ dimmed2.borrow_mut().clear();
if let Some(s) = ctx2.service.borrow().as_ref() {
s.activate(&w2);
}
pop2.popdown();
});
+
+ // hover-effekt per rad: peek eller fokus
+ match list_hover {
+ ListHover::Peek if peek_possible => {
+ let mc = gtk::EventControllerMotion::new();
+ let w3 = w.clone();
+ let dimmed3 = dimmed.clone();
+ mc.connect_enter(move |_, _, _| {
+ peek_restore(&dimmed3.borrow());
+ *dimmed3.borrow_mut() = peek_apply(&w3);
+ });
+ let dimmed4 = dimmed.clone();
+ mc.connect_leave(move |_| {
+ peek_restore(&dimmed4.borrow());
+ dimmed4.borrow_mut().clear();
+ });
+ row.add_controller(mc);
+ }
+ ListHover::Focus => {
+ let mc = gtk::EventControllerMotion::new();
+ let ctx3 = ctx.clone();
+ let w3 = w.clone();
+ mc.connect_enter(move |_, _, _| {
+ if let Some(s) = ctx3.service.borrow().as_ref() {
+ s.activate(&w3);
+ }
+ });
+ row.add_controller(mc);
+ }
+ _ => {}
+ }
list.append(&row);
}
popover.set_child(Some(&list));
popover.set_parent(anchor);
+
+ // säkerhetsnät: återställ peek när popovern stängs
+ {
+ let dimmed5 = dimmed.clone();
+ popover.connect_closed(move |_| {
+ peek_restore(&dimmed5.borrow());
+ dimmed5.borrow_mut().clear();
+ });
+ }
+
+ // i hover-läge: stäng listan när pekaren lämnar den
+ if hover_mode {
+ let mc = gtk::EventControllerMotion::new();
+ let pop3 = popover.clone();
+ mc.connect_leave(move |ctl| {
+ let pop4 = pop3.clone();
+ let ctl2 = ctl.clone();
+ glib::timeout_add_local_once(std::time::Duration::from_millis(400), move || {
+ if !ctl2.contains_pointer() {
+ pop4.popdown();
+ }
+ });
+ });
+ popover.add_controller(mc);
+ }
+
autodispose(&popover);
popover.popup();
}
diff --git a/src/settings.rs b/src/settings.rs
index 50aae62..dacb35b 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -14,14 +14,16 @@ use gtk4::prelude::*;
use log::warn;
use crate::config::{
- self, AutohideMode, ClickAction, HotspotAction, IndicatorStyle, Layer, MenuItem,
- MiddleClick, Position, ScrollAction,
+ self, Alignment, AutohideMode, ClickAction, GroupPopup, HotspotAction, IndicatorStyle,
+ Layer, ListHover, MenuItem, MiddleClick, Position, ScrollAction,
};
use crate::dock::Ctx;
thread_local! {
static OPEN_WINDOW: std::cell::RefCell