Fixa omladdningsloopen + churn; CI/släpp flyttat till x64-byggnoden
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:
2026-08-29 23:51:27 +02:00
parent 0ded844b3a
commit e4e4d39405
10 changed files with 230 additions and 70 deletions

View File

@@ -1,7 +1,8 @@
name: check
# Kompilerings- och lintvakt på Pi5-runnern (arm64). Bygger inga
# artefakter — släpp byggs lokalt på brasse-linux01 via ./release.sh.
# Kompilerings- och lintvakt på byggnoden på brasse-linux01 (x64-runnern
# med exklusiva labeln linux-amd64). waydock byggs BARA för x86_64 —
# aldrig på Pi5:ns arm-runner.
on:
push:
branches: [master]
@@ -14,7 +15,7 @@ on:
jobs:
check:
runs-on: ubuntu-latest
runs-on: linux-amd64
steps:
- name: Checkout
uses: actions/checkout@v4

View File

@@ -0,0 +1,101 @@
name: release
# Släppbygge på byggnoden på brasse-linux01 (x64-runnern, label
# linux-amd64). En vX.Y.Z-tagg ger en versionerad release + uppdaterar
# rullande "latest". Bara linux-x64 byggs — waydock är en desktopapp
# för brasse-linux01, ingen arm-build.
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Release-tag (lämna tom för 'latest')"
required: false
jobs:
build-release:
runs-on: linux-amd64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Bestäm tag
id: t
run: |
case "$GITHUB_REF" in
refs/tags/*) T="$GITHUB_REF_NAME" ;;
*) T="${{ inputs.tag }}"; [ -z "$T" ] && T="latest" ;;
esac
echo "tag=$T" >> "$GITHUB_OUTPUT"
echo "bygger release: $T"
- name: Install toolchain + GTK4 deps
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq curl build-essential pkg-config git jq \
libgtk-4-dev meson ninja-build wayland-protocols libwayland-dev
if ! apt-get install -y -qq libgtk4-layer-shell-dev; then
git clone --depth 1 https://github.com/wmww/gtk4-layer-shell.git /tmp/g4ls
meson setup /tmp/g4ls/build /tmp/g4ls \
-Dintrospection=false -Dvapi=false -Dexamples=false \
-Ddocs=false -Dtests=false
ninja -C /tmp/g4ls/build install
ldconfig
arch_dir=$(gcc -dumpmachine)
echo "PKG_CONFIG_PATH=/usr/local/lib/${arch_dir}/pkgconfig:/usr/local/lib/pkgconfig" >> "$GITHUB_ENV"
echo "LD_LIBRARY_PATH=/usr/local/lib/${arch_dir}:/usr/local/lib" >> "$GITHUB_ENV"
fi
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Bygg x64-binären
run: |
cargo build --release --locked
cargo test --locked
mkdir -p out
cp target/release/waydock "out/waydock-${{ steps.t.outputs.tag }}-linux-x64"
cp target/release/waydock out/waydock-latest-linux-x64
cp install.sh out/install.sh
cd out && sha256sum waydock-* install.sh > checksums.txt && ls -la
- name: Skapa Gitea-release + ladda upp assets
env:
API: ${{ github.server_url }}/api/v1
REPO: ${{ github.repository }}
TAG: ${{ steps.t.outputs.tag }}
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BODY="waydock $TAG — linux-x64. Installera: curl -fsSL https://gitea.brasse-pc.eu/brasse/waydock/raw/branch/master/install.sh | sh"
publish() { # $1 = releasetagg, $2... = assetfiler
local tag=$1; shift
local rid
rid=$(curl -s -X POST "$API/repos/$REPO/releases" \
-H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
-d "{\"tag_name\":\"$tag\",\"name\":\"waydock $tag\",\"body\":\"$BODY\"}" | jq -r '.id // empty')
[ -z "$rid" ] && rid=$(curl -s "$API/repos/$REPO/releases/tags/$tag" \
-H "Authorization: token $TOKEN" | jq -r '.id')
echo "release $tag -> id $rid"
for f in "$@"; do
local aid
aid=$(curl -s "$API/repos/$REPO/releases/$rid/assets" \
-H "Authorization: token $TOKEN" | jq -r ".[] | select(.name==\"$f\") | .id")
if [ -n "$aid" ] && [ "$aid" != "null" ]; then
curl -s -X DELETE "$API/repos/$REPO/releases/$rid/assets/$aid" \
-H "Authorization: token $TOKEN" -o /dev/null
fi
curl -s -X POST "$API/repos/$REPO/releases/$rid/assets?name=$f" \
-H "Authorization: token $TOKEN" -F "attachment=@out/$f" \
-o /dev/null -w " $f -> HTTP %{http_code}\n"
done
}
if [ "$TAG" != "latest" ]; then
publish "$TAG" "waydock-$TAG-linux-x64" install.sh checksums.txt
fi
# rullande latest: flytta taggen till HEAD och publicera om
git config user.name gitea-actions
git config user.email actions@gitea.brasse-pc.eu
git tag -f latest
git push -f origin latest || echo "kunde inte flytta latest-taggen (fortsätter ändå)"
publish latest waydock-latest-linux-x64 install.sh checksums.txt

View File

@@ -55,7 +55,12 @@ scale/expo-toggle och fönstergeometri till intelligent autohide.
## Släpp
```bash
./release.sh v0.1.0 # taggar, bygger x64, skapar Gitea-release + latest
./release.sh v0.1.0 # taggar och pushar — CI bygger och publicerar
```
CI på Gitea (Pi5) kör `cargo check`/`clippy`/`test` vid varje push.
All CI kör på **byggnoden på brasse-linux01** (x64-runnern, label
`linux-amd64`) — waydock byggs bara för x86_64, aldrig på Pi5:ns
arm-runner. Push till master kör `cargo check`/`clippy`/`test`; en
vX.Y.Z-tagg bygger release-binären och laddar upp den till
[släppsidan](https://gitea.brasse-pc.eu/brasse/waydock/releases)
(+ rullande `latest`).

View File

@@ -26,11 +26,12 @@ inbyggt settings-GUI, med live-uppdatering åt båda hållen.
Pattern A (binär), med lokalt släppbygge:
- **CI (Gitea Actions, Pi5/arm64):** `cargo check` + `clippy` + `test`
vid push till master — kompilerings-/lintvakt, inga artefakter.
- **Släpp:** `./release.sh vX.Y.Z` på brasse-linux01 — taggar, bygger
x64-release, skapar Gitea-release med binären + uppdaterar rullande
`latest`. `install.sh` ger en-rads-install via curl.
- **CI (Gitea Actions, x64-runnern `linux-amd64` på brasse-linux01):**
`cargo check` + `clippy` + `test` vid push till master. Bara x86_64 —
ingen arm-build (Pi5-runnern rör aldrig det här repot).
- **Släpp:** `./release.sh vX.Y.Z` taggar och pushar; release-workflowet
på byggnoden bygger x64-binären och laddar upp till Gitea-släppsidan
+ uppdaterar rullande `latest`. `install.sh` ger en-rads-install.
## Config

View File

@@ -1,56 +1,24 @@
#!/usr/bin/env bash
# Lokalt släppbygge för waydock (körs på brasse-linux01, x64).
# Släpp waydock: taggar vX.Y.Z och pushar — själva bygget och uppladdningen
# till Gitea-releasen görs av CI-jobbet .gitea/workflows/release.yaml på
# byggnoden (x64-runnern på brasse-linux01).
# ./release.sh v0.2.0
# Taggar, bygger release-binären, skapar Gitea-release med binären som
# asset och uppdaterar den rullande "latest"-releasen.
set -euo pipefail
TAG="${1:?användning: ./release.sh vX.Y.Z}"
[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "taggen ska vara vX.Y.Z"; exit 1; }
OWNER=brasse REPO=waydock
API=https://gitea.brasse-pc.eu/api/v1
TOKEN="${GITEA_TOKEN:-$(jq -r .token ~/.config/giteactl/config.json)}"
BIN=target/release/waydock
ASSET="waydock-${TAG}-linux-x64"
# versionen i Cargo.toml ska matcha taggen
CARGO_VER=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2)
[[ "v$CARGO_VER" == "$TAG" ]] || { echo "Cargo.toml har $CARGO_VER, taggen är $TAG — uppdatera först"; exit 1; }
echo "==> bygger $TAG"
echo "==> snabbkoll innan tagg"
cargo build --release --locked
cargo test --locked
echo "==> taggar + pushar"
echo "==> taggar + pushar (CI bygger och publicerar releasen)"
git tag -a "$TAG" -m "waydock $TAG"
git push origin master "$TAG"
api() { # method path [json|@fil]
local m=$1 p=$2; shift 2
curl -fsS -X "$m" -H "Authorization: token $TOKEN" "$@" "$API$p"
}
make_release() { # tag namn prerelease
api POST "/repos/$OWNER/$REPO/releases" \
-H 'Content-Type: application/json' \
-d "{\"tag_name\":\"$1\",\"name\":\"$2\",\"body\":\"waydock $TAG — linux-x64.\\n\\nInstallera:\\n\`\`\`\\ncurl -fsSL https://gitea.brasse-pc.eu/brasse/waydock/raw/branch/master/install.sh | sh\\n\`\`\`\"}"
}
upload() { # release-id filnamn
api POST "/repos/$OWNER/$REPO/releases/$1/assets?name=$2" \
-F "attachment=@$BIN;filename=$2" > /dev/null
}
echo "==> skapar release $TAG"
REL_ID=$(make_release "$TAG" "waydock $TAG" | jq .id)
upload "$REL_ID" "$ASSET"
echo "==> uppdaterar rullande latest"
OLD=$(api GET "/repos/$OWNER/$REPO/releases/tags/latest" 2>/dev/null | jq -r .id) || OLD=""
[[ -n "$OLD" && "$OLD" != null ]] && api DELETE "/repos/$OWNER/$REPO/releases/$OLD" || true
git tag -f latest && git push -f origin latest
LATEST_ID=$(make_release "latest" "waydock latest ($TAG)" | jq .id)
upload "$LATEST_ID" "waydock-latest-linux-x64"
echo "==> klart: https://gitea.brasse-pc.eu/$OWNER/$REPO/releases"
echo "==> följ bygget: giteactl runs waydock"
echo "==> releaser: https://gitea.brasse-pc.eu/brasse/waydock/releases"

View File

@@ -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(());
}

View File

@@ -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

View File

@@ -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();
}

View File

@@ -77,7 +77,7 @@ fn row(list: &gtk::Box, label: &str, widget: &impl IsA<gtk::Widget>) {
fn heading(list: &gtk::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);

View File

@@ -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);