Fixa omladdningsloopen + churn; CI/släpp flyttat till x64-byggnoden
Some checks failed
check / check (push) Failing after 1m4s
Some checks failed
check / check (push) Failing after 1m4s
- KRITISKT: notify-watchern reagerade på Access-events, så vår egen läsning av configen triggade ny omladdning i all oändlighet (var 155:e ms). Panelen revs och byggdes om konstant → blinkande hover, klick som aldrig landade. Nu filtreras eventen till riktiga skrivningar (Modify/Create/Remove/Close-Write). - Titeländringar (webbläsare/terminaler) bygger inte längre om UI:t; tooltips slås i stället upp färskt vid hover (query-tooltip), och klick/skroll/menyer agerar på färskt fönster-state vid eventtillfället. - hotspots::rebuild är idempotent (skippar om config+skärmar oförändrade). - Settings: markup-escape i rubriker (Kant & skugga-varningen). - CI: check + release körs på byggnoden brasse-linux01 (runs-on linux-amd64) — waydock byggs BARA för x86_64, aldrig på Pi5:ns arm-runner. Ny release.yaml: vX.Y.Z-tagg bygger x64-binären och laddar upp till Gitea-släppsidan + rullande latest. release.sh är nu bara tagga-och-pusha. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -389,11 +389,23 @@ pub fn watch(path: &Path, on_change: Rc<dyn Fn()>) {
|
||||
let dir = path.parent().unwrap_or(Path::new(".")).to_path_buf();
|
||||
|
||||
let mut watcher = match notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
|
||||
use notify::event::{AccessKind, AccessMode, EventKind};
|
||||
if let Ok(ev) = res {
|
||||
let hit = ev
|
||||
.paths
|
||||
.iter()
|
||||
.any(|p| p.file_name().map(|s| s.to_os_string()) == file_name);
|
||||
// VIKTIGT: bara riktiga skrivningar. Access-events (öppna/läsa)
|
||||
// måste ignoreras — annars triggar vår egen läsning en ny
|
||||
// omladdning i all oändlighet.
|
||||
let relevant = matches!(
|
||||
ev.kind,
|
||||
EventKind::Modify(_)
|
||||
| EventKind::Create(_)
|
||||
| EventKind::Remove(_)
|
||||
| EventKind::Access(AccessKind::Close(AccessMode::Write))
|
||||
);
|
||||
let hit = relevant
|
||||
&& ev
|
||||
.paths
|
||||
.iter()
|
||||
.any(|p| p.file_name().map(|s| s.to_os_string()) == file_name);
|
||||
if hit {
|
||||
let _ = tx.send_blocking(());
|
||||
}
|
||||
|
||||
69
src/dock.rs
69
src/dock.rs
@@ -415,6 +415,33 @@ pub fn recompute_dodge(ctx: &Rc<Ctx>) {
|
||||
update_autohide(ctx);
|
||||
}
|
||||
|
||||
/// Färska fönster för en ikon — slås upp vid klick-/tooltip-tillfället så
|
||||
/// att closures aldrig agerar på inaktuellt state.
|
||||
pub fn fresh_windows(ctx: &Ctx, connector: &str, key: &str) -> Vec<ToplevelInfo> {
|
||||
let service = ctx.service.borrow();
|
||||
let Some(service) = service.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let only_here = ctx.cfg.borrow().only_this_output(connector);
|
||||
service
|
||||
.snapshot()
|
||||
.into_iter()
|
||||
.filter(|t| {
|
||||
(!only_here || t.outputs.iter().any(|o| o == connector))
|
||||
&& icons::lookup(&t.app_id).key == key
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Bygg en färsk kopia av en ikon vid interaktionstillfället.
|
||||
fn live_item(ctx: &Ctx, connector: &str, item: &Item) -> Rc<Item> {
|
||||
Rc::new(Item {
|
||||
meta: item.meta.clone(),
|
||||
windows: fresh_windows(ctx, connector, &item.meta.key),
|
||||
pinned: item.pinned,
|
||||
})
|
||||
}
|
||||
|
||||
/// Gruppera snapshot → ikonrad för en viss skärm.
|
||||
fn build_items(ctx: &Ctx, connector: &str, snapshot: &[ToplevelInfo]) -> Vec<Item> {
|
||||
let cfg = ctx.cfg.borrow();
|
||||
@@ -459,7 +486,7 @@ pub fn refresh_items(ctx: &Rc<Ctx>) {
|
||||
}
|
||||
add_special_buttons(ctx, dock);
|
||||
for item in build_items(ctx, &dock.connector, &snapshot) {
|
||||
let widget = build_item_widget(ctx, Rc::new(item));
|
||||
let widget = build_item_widget(ctx, &dock.connector, Rc::new(item));
|
||||
dock.panel.append(&widget);
|
||||
}
|
||||
}
|
||||
@@ -496,7 +523,7 @@ fn add_special_buttons(ctx: &Rc<Ctx>, dock: &Rc<Dock>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_item_widget(ctx: &Rc<Ctx>, item: Rc<Item>) -> gtk::Button {
|
||||
fn build_item_widget(ctx: &Rc<Ctx>, connector: &str, item: Rc<Item>) -> gtk::Button {
|
||||
let cfg = ctx.cfg.borrow();
|
||||
let icon_size = cfg.appearance.icon_size;
|
||||
let max_dots = cfg.appearance.indicator.max as usize;
|
||||
@@ -543,15 +570,23 @@ fn build_item_widget(ctx: &Rc<Ctx>, item: Rc<Item>) -> gtk::Button {
|
||||
btn.set_child(Some(&vbox));
|
||||
|
||||
if tooltips {
|
||||
let tip = match item.windows.len() {
|
||||
0 => item.meta.display_name.clone(),
|
||||
1 => {
|
||||
let t = &item.windows[0].title;
|
||||
if t.is_empty() { item.meta.display_name.clone() } else { t.clone() }
|
||||
}
|
||||
n => format!("{} — {n} fönster", item.meta.display_name),
|
||||
};
|
||||
btn.set_tooltip_text(Some(&tip));
|
||||
btn.set_has_tooltip(true);
|
||||
let ctx2 = ctx.clone();
|
||||
let item2 = item.clone();
|
||||
let conn2 = connector.to_string();
|
||||
btn.connect_query_tooltip(move |_, _, _, _, tooltip| {
|
||||
let windows = fresh_windows(&ctx2, &conn2, &item2.meta.key);
|
||||
let tip = match windows.len() {
|
||||
0 => item2.meta.display_name.clone(),
|
||||
1 => {
|
||||
let t = &windows[0].title;
|
||||
if t.is_empty() { item2.meta.display_name.clone() } else { t.clone() }
|
||||
}
|
||||
n => format!("{} — {n} fönster", item2.meta.display_name),
|
||||
};
|
||||
tooltip.set_text(Some(&tip));
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
// klick: vänster/mitt/höger
|
||||
@@ -561,12 +596,14 @@ fn build_item_widget(ctx: &Rc<Ctx>, item: Rc<Item>) -> gtk::Button {
|
||||
let ctx = ctx.clone();
|
||||
let item = item.clone();
|
||||
let btn2 = btn.clone();
|
||||
let conn2 = connector.to_string();
|
||||
click.connect_released(move |gesture, _, _, _| {
|
||||
gesture.set_state(gtk::EventSequenceState::Claimed);
|
||||
let live = live_item(&ctx, &conn2, &item);
|
||||
match gesture.current_button() {
|
||||
1 => primary_action(&ctx, &item, &btn2),
|
||||
2 => middle_action(&ctx, &item),
|
||||
3 => menu::popup_item_menu(&ctx, &item, &btn2),
|
||||
1 => primary_action(&ctx, &live, &btn2),
|
||||
2 => middle_action(&ctx, &live),
|
||||
3 => menu::popup_item_menu(&ctx, &live, &btn2),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
@@ -578,9 +615,11 @@ fn build_item_widget(ctx: &Rc<Ctx>, item: Rc<Item>) -> gtk::Button {
|
||||
{
|
||||
let ctx = ctx.clone();
|
||||
let item = item.clone();
|
||||
let conn2 = connector.to_string();
|
||||
scroll.connect_scroll(move |_, _, dy| {
|
||||
if ctx.cfg.borrow().behavior.scroll == ScrollAction::CycleWindows {
|
||||
cycle_windows(&ctx, &item, dy > 0.0);
|
||||
let live = live_item(&ctx, &conn2, &item);
|
||||
cycle_windows(&ctx, &live, dy > 0.0);
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
|
||||
@@ -101,8 +101,24 @@ fn build_hotspot(
|
||||
win
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static LAST_SIGNATURE: std::cell::RefCell<String> =
|
||||
const { std::cell::RefCell::new(String::new()) };
|
||||
}
|
||||
|
||||
/// Bygg om alla hotspot-ytor enligt configen (körs vid start + configbyte).
|
||||
/// Idempotent: oförändrad hotspot-config + skärmuppsättning → ingen ombyggnad.
|
||||
pub fn rebuild(ctx: &Rc<Ctx>) {
|
||||
let n_monitors = gdk::Display::default()
|
||||
.map(|d| d.monitors().n_items())
|
||||
.unwrap_or(0);
|
||||
let signature = format!("{:?}|{n_monitors}", ctx.cfg.borrow().hotspots);
|
||||
let unchanged = LAST_SIGNATURE.with(|s| *s.borrow() == signature);
|
||||
if unchanged {
|
||||
return;
|
||||
}
|
||||
LAST_SIGNATURE.with(|s| *s.borrow_mut() = signature);
|
||||
|
||||
for win in ctx.hotspot_windows.borrow_mut().drain(..) {
|
||||
win.destroy();
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ fn row(list: >k::Box, label: &str, widget: &impl IsA<gtk::Widget>) {
|
||||
|
||||
fn heading(list: >k::Box, text: &str) {
|
||||
let lbl = gtk::Label::new(None);
|
||||
lbl.set_markup(&format!("<b>{text}</b>"));
|
||||
lbl.set_markup(&format!("<b>{}</b>", glib::markup_escape_text(text)));
|
||||
lbl.set_halign(gtk::Align::Start);
|
||||
lbl.set_margin_top(10);
|
||||
list.append(&lbl);
|
||||
|
||||
@@ -43,6 +43,7 @@ struct ToplevelEntry {
|
||||
activated: bool,
|
||||
outputs: Vec<ObjectId>,
|
||||
initialized: bool, // första done har kommit
|
||||
changed: bool, // något UI-relevant ändrat sedan senaste done
|
||||
}
|
||||
|
||||
struct OutputEntry {
|
||||
@@ -169,6 +170,7 @@ impl Dispatch<ZwlrForeignToplevelManagerV1, ()> for WlState {
|
||||
activated: false,
|
||||
outputs: Vec::new(),
|
||||
initialized: false,
|
||||
changed: false,
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -199,9 +201,12 @@ impl Dispatch<ZwlrForeignToplevelHandleV1, ()> for WlState {
|
||||
Event::AppId { app_id } => {
|
||||
if let Some(e) = state.entry_mut(&id) {
|
||||
e.app_id = app_id;
|
||||
e.changed = true;
|
||||
}
|
||||
}
|
||||
Event::Title { title } => {
|
||||
// titlar byts hela tiden (webbläsare, terminaler) — spara
|
||||
// dem men bygg inte om UI:t; tooltips slås upp färskt ändå
|
||||
if let Some(e) = state.entry_mut(&id) {
|
||||
e.title = title;
|
||||
}
|
||||
@@ -209,11 +214,13 @@ impl Dispatch<ZwlrForeignToplevelHandleV1, ()> for WlState {
|
||||
Event::OutputEnter { output } => {
|
||||
if let Some(e) = state.entry_mut(&id) {
|
||||
e.outputs.push(output.id());
|
||||
e.changed = true;
|
||||
}
|
||||
}
|
||||
Event::OutputLeave { output } => {
|
||||
if let Some(e) = state.entry_mut(&id) {
|
||||
e.outputs.retain(|o| *o != output.id());
|
||||
e.changed = true;
|
||||
}
|
||||
}
|
||||
Event::State { state: raw } => {
|
||||
@@ -231,16 +238,26 @@ impl Dispatch<ZwlrForeignToplevelHandleV1, ()> for WlState {
|
||||
}
|
||||
}
|
||||
if let Some(e) = state.entry_mut(&id) {
|
||||
if e.minimized != min || e.maximized != max || e.activated != act {
|
||||
e.changed = true;
|
||||
}
|
||||
e.minimized = min;
|
||||
e.maximized = max;
|
||||
e.activated = act;
|
||||
}
|
||||
}
|
||||
Event::Done => {
|
||||
let mut mark_dirty = false;
|
||||
if let Some(e) = state.entry_mut(&id) {
|
||||
if !e.initialized || e.changed {
|
||||
mark_dirty = true;
|
||||
}
|
||||
e.initialized = true;
|
||||
e.changed = false;
|
||||
}
|
||||
if mark_dirty {
|
||||
state.dirty = true;
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
Event::Closed => {
|
||||
state.toplevels.retain(|(tid, _)| *tid != id);
|
||||
|
||||
Reference in New Issue
Block a user