{"id":49638985,"url":"https://github.com/yolo-labz/noctalia-appmenu","last_synced_at":"2026-06-07T03:01:36.048Z","repository":{"id":355716330,"uuid":"1229270623","full_name":"yolo-labz/noctalia-appmenu","owner":"yolo-labz","description":"macOS-style global menu for noctalia-shell on niri. Rust sidecar bridge + Quickshell QML widget.","archived":false,"fork":false,"pushed_at":"2026-06-02T03:20:37.000Z","size":1241,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-02T04:23:30.686Z","etag":null,"topics":["appmenu","dbusmenu","home-manager","niri","nixos","noctalia","qml","quickshell","rust","wayland"],"latest_commit_sha":null,"homepage":"https://github.com/yolo-labz/noctalia-appmenu","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yolo-labz.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["phsb5321"]}},"created_at":"2026-05-04T21:53:13.000Z","updated_at":"2026-06-02T03:20:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yolo-labz/noctalia-appmenu","commit_stats":null,"previous_names":["yolo-labz/noctalia-appmenu"],"tags_count":59,"template":false,"template_full_name":null,"purl":"pkg:github/yolo-labz/noctalia-appmenu","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yolo-labz%2Fnoctalia-appmenu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yolo-labz%2Fnoctalia-appmenu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yolo-labz%2Fnoctalia-appmenu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yolo-labz%2Fnoctalia-appmenu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yolo-labz","download_url":"https://codeload.github.com/yolo-labz/noctalia-appmenu/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yolo-labz%2Fnoctalia-appmenu/sbom","scorecard":{"id":1246972,"data":{"date":"2026-05-04T23:35:07Z","repo":{"name":"github.com/yolo-labz/noctalia-appmenu","commit":"005ddb1c5710c26e54ac6388fe68f2a529568872"},"scorecard":{"version":"v5.3.0","commit":"c22063e786c11f9dd714d777a687ff7c4599b600"},"score":6.3,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/3 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#code-review"}},{"name":"Dependency-Update-Tool","score":10,"reason":"update tool detected","details":["Info: detected update tool: Dependabot: .github/dependabot.yml:1"],"documentation":{"short":"Determines if the project uses a dependency update tool.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#dependency-update-tool"}},{"name":"Maintained","score":0,"reason":"project was created within the last 90 days. Please review its contents carefully","details":["Warn: Repository was created within the last 90 days."],"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#maintained"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: SECURITY.md:1","Info: Found linked content: SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: SECURITY.md:1","Info: Found text in security policy: SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#security-policy"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#packaging"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":10,"reason":"GitHub workflow tokens follow principle of least privilege","details":["Info: jobLevel 'contents' permission set to 'read': .github/workflows/actionlint.yml:24","Info: jobLevel 'contents' permission set to 'read': .github/workflows/cargo-deny.yml:27","Info: jobLevel 'contents' permission set to 'read': .github/workflows/cargo-machete.yml:24","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:52","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:83","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:106","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:23","Info: jobLevel 'contents' permission set to 'read': .github/workflows/codecov.yml:20","Info: jobLevel 'contents' permission set to 'read': .github/workflows/codeql.yml:23","Info: jobLevel 'actions' permission set to 'read': .github/workflows/codeql.yml:22","Info: jobLevel 'contents' permission set to 'read': .github/workflows/dco.yml:18","Info: jobLevel 'pull-requests' permission set to 'read': .github/workflows/dco.yml:19","Info: jobLevel 'contents' permission set to 'read': .github/workflows/osv-scan.yml:20","Info: jobLevel 'contents' permission set to 'read': .github/workflows/pages.yml:28","Info: jobLevel 'contents' permission set to 'read': .github/workflows/perf.yml:28","Info: jobLevel 'contents' permission set to 'read': .github/workflows/perf.yml:55","Warn: jobLevel 'contents' permission set to 'write': .github/workflows/release.yml:18","Info: jobLevel 'contents' permission set to 'read': .github/workflows/reproducibility.yml:20","Info: jobLevel 'contents' permission set to 'read': .github/workflows/scorecard.yml:20","Info: jobLevel 'actions' permission set to 'read': .github/workflows/scorecard.yml:21","Info: jobLevel 'contents' permission set to 'read': .github/workflows/semgrep.yml:20","Info: jobLevel 'contents' permission set to 'read': .github/workflows/sonar.yml:20","Info: jobLevel 'pull-requests' permission set to 'read': .github/workflows/sonar.yml:21","Info: jobLevel 'contents' permission set to 'read': .github/workflows/typos.yml:20","Info: found token with 'none' permissions: .github/workflows/actionlint.yml:1","Info: found token with 'none' permissions: .github/workflows/cargo-deny.yml:1","Info: found token with 'none' permissions: .github/workflows/cargo-machete.yml:1","Info: found token with 'none' permissions: .github/workflows/ci.yml:1","Info: found token with 'none' permissions: .github/workflows/codecov.yml:1","Info: found token with 'none' permissions: .github/workflows/codeql.yml:1","Info: found token with 'none' permissions: .github/workflows/dco.yml:1","Info: found token with 'none' permissions: .github/workflows/osv-scan.yml:1","Info: found token with 'none' permissions: .github/workflows/pages.yml:1","Info: found token with 'none' permissions: .github/workflows/perf.yml:1","Info: found token with 'none' permissions: .github/workflows/release.yml:1","Info: found token with 'none' permissions: .github/workflows/reproducibility.yml:1","Info: found token with 'none' permissions: .github/workflows/scorecard.yml:1","Info: found token with 'none' permissions: .github/workflows/semgrep.yml:1","Info: topLevel 'contents' permission set to 'read': .github/workflows/sonar.yml:11","Info: topLevel 'pull-requests' permission set to 'read': .github/workflows/sonar.yml:12","Info: found token with 'none' permissions: .github/workflows/typos.yml:1"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":10,"reason":"all dependencies are pinned","details":["Info:  36 out of  36 GitHub-owned GitHubAction dependencies pinned","Info:  27 out of  27 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#pinned-dependencies"}},{"name":"SAST","score":8,"reason":"SAST tool detected but not run on all commits","details":["Info: SAST configuration detected: CodeQL","Warn: 1 commits out of 2 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#sast"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#cii-best-practices"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#signed-releases"}},{"name":"Vulnerabilities","score":9,"reason":"1 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: RUSTSEC-2025-0141"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#vulnerabilities"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#branch-protection"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#fuzzing"}},{"name":"CI-Tests","score":10,"reason":"2 out of 2 merged PRs checked by a CI test -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project runs tests before pull requests are merged.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#ci-tests"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#license"}},{"name":"Contributors","score":0,"reason":"project has 0 contributing companies or organizations -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project has a set of contributors from multiple organizations (e.g., companies).","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#contributors"}}]},"last_synced_at":"2026-05-05T00:32:03.625Z","repository_id":355716330,"created_at":"2026-05-05T00:32:03.625Z","updated_at":"2026-05-05T00:32:03.625Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34006056,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-07T02:00:07.652Z","response_time":124,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["appmenu","dbusmenu","home-manager","niri","nixos","noctalia","qml","quickshell","rust","wayland"],"created_at":"2026-05-05T18:00:58.591Z","updated_at":"2026-06-07T03:01:36.032Z","avatar_url":"https://github.com/yolo-labz.png","language":"Rust","funding_links":["https://github.com/sponsors/phsb5321"],"categories":[],"sub_categories":[],"readme":"# noctalia-appmenu\n\nmacOS-style global menu for [noctalia-shell](https://github.com/noctalia-dev/noctalia-shell) on [niri](https://github.com/YaLTeR/niri).\n\nWhen you focus a Qt or GTK application, its menubar (`File`, `Edit`, `View`, …) appears in noctalia's topbar instead of inside the window. The behaviour mirrors macOS and Plasma's `appmenu` applet.\n\n\u003e **Status:** v1.0.0 release candidate. AT-SPI substrate ([ADR-0024](docs/adr/ADR-0024-atspi-substrate.md)) replaces the v0.1 DBusMenu/Registrar pipeline. Qt6 primary; GTK4 secondary. Firefox/Electron supported via documented toolkit flags — see [Caveats](#caveats). niri only by design ([ADR-0005](docs/adr/ADR-0005-niri-only-v1.md)); compositor-abstraction door is open but unwired ([spec 004 FR-003](specs/004-project-completion/spec.md)).\n\n## How it works\n\nThree pieces collaborate:\n\n| Component | Role |\n|---|---|\n| **`at-spi2-core`** (`services.gnome.at-spi2-core.enable = true`) | System-wide accessibility bus. Qt and GTK toolkits export their menu structure here when `QT_ACCESSIBILITY=1` / GTK a11y is active. |\n| **`noctalia-appmenu-bridge`** (this repo, Rust) | Sidecar daemon. Subscribes to niri's IPC event-stream for focus changes, walks the focused application's AT-SPI accessibility tree to extract its menubar, and writes the snapshot to `~/.cache/noctalia-appmenu/active.json` (schema v=1, [ADR-0023](docs/adr/ADR-0023-dbusmenu-fetch-on-focus.md)) plus a fixed D-Bus address (`org.noctalia.AppMenu /org/noctalia/AppMenu/Active`). |\n| **`noctalia-appmenu` plugin** (this repo, QML) | A noctalia bar widget that subscribes to the snapshot file and renders the menu strip in the topbar. Click events are forwarded back to the bridge, which invokes `AT-SPI DoAction` against the original accessible. |\n\nThe bridge exists because Quickshell's `DBusMenuHandle` is `QML_UNCREATABLE` — there is no public way to bind QML to an arbitrary `(busName, objectPath)` pair. The bridge mirrors the active app's menu at a fixed path that the QML widget can attach to. See [ADR-0007](docs/adr/ADR-0007-fixed-proxy-vs-quickshell-pr.md) for the original constraint and [ADR-0024](docs/adr/ADR-0024-atspi-substrate.md) for the substrate decision.\n\n## Install (NixOS / Home-Manager)\n\n```nix\n# flake.nix\n{\n  inputs.noctalia-appmenu.url = \"github:yolo-labz/noctalia-appmenu\";\n\n  outputs = { home-manager, noctalia-appmenu, ... }: {\n    homeConfigurations.\"pedro@desktop\" = home-manager.lib.homeManagerConfiguration {\n      modules = [\n        noctalia-appmenu.homeManagerModules.default\n        {\n          programs.noctalia.plugins.appmenu = {\n            enable = true;\n            # `registrar` is deprecated in v1.0.0 — the AT-SPI substrate\n            # does not use it. The option is recognised for one cycle\n            # so existing configs do not break; it is removed in v1.1.\n          };\n        }\n      ];\n    };\n  };\n}\n```\n\nThe Home-Manager module installs the bridge binary, the QML plugin payload, the hardened `systemd --user` unit (`noctalia-appmenu-bridge.service`), and exports `QT_ACCESSIBILITY=1` into the session environment. System-level prerequisites (the AT-SPI bus) must be enabled at the NixOS layer separately — see [Verify the install](#verify-the-install) §1.\n\n## Compatibility\n\n| Toolkit | Status | Notes |\n|---|---|---|\n| Qt6 (KDE Frameworks apps, Anki, Telegram, Krita, qutebrowser) | Works | Requires `QT_ACCESSIBILITY=1` in session env (set automatically by the HM module). |\n| GTK3 / GTK4 | Works | GTK4 `GtkPopoverMenuBar` (Nautilus 45+) exposes `MENU_BAR` with zero children when the menu is closed; the bridge then serves the [desktop fallback](#app-menu-fallback) (`source = \"desktop-fallback\"`). |\n| XWayland Qt5/GTK | Works | AT-SPI walker is toolkit-agnostic; X11 windowing does not interfere. |\n| Electron / Chromium | Fallback (full menu via flag) | No native menubar by default → the bridge serves the [desktop fallback](#app-menu-fallback) (app actions + window controls). Launch with `--force-accessibility` to expose the real menubar. |\n| Firefox / Thunderbird | Fallback only (on niri) | Firefox's AT-SPI menubar is **lazy + reveal-locked**: its items realise only when the menu is *visibly opened*, and on niri that reveal cannot be undone ([ADR-0035](docs/adr/ADR-0035-lazy-reveal-locked-menubar-fallback.md)). The bridge serves the honest [desktop fallback](#app-menu-fallback) instead of pinning a duplicate menubar. A clean Firefox global menu needs niri to advertise `org_kde_kwin_appmenu_manager` (the [niri-protocol path](#firefox--thunderbird)) — a compositor change, not reachable from the bridge. |\n| libcosmic / Iced (`cosmic-files`, …) | Fallback only | No upstream AT-SPI export → [desktop fallback](#app-menu-fallback) always. Tracked at [#157](https://github.com/yolo-labz/noctalia-appmenu/issues/157). |\n\n## Verify the install\n\nReproduces [`specs/004-project-completion/quickstart.md`](specs/004-project-completion/quickstart.md) condensed for a fresh-NixOS user. Time budget: ≤ 10 min from a clean shell to a working Anki menubar.\n\n### 1. System prerequisites\n\nYou need:\n\n- NixOS 25.05 or newer (any channel with `niri \u003e= 25.04` and `at-spi2-core \u003e= 2.50`).\n- `niri` running as your Wayland compositor.\n- `noctalia-shell \u003e= 1.0.0` running.\n- A login session that activates the `graphical-session.target` systemd user target (the default under `niri`).\n\nIn your NixOS configuration:\n\n```nix\n{\n  services.gnome.at-spi2-core.enable = true;\n}\n```\n\nRebuild + activate:\n\n```bash\nsudo nixos-rebuild switch\n```\n\nVerify:\n\n```bash\nniri msg version | head -1\n# expect: niri 25.xx\nqs --version | head -1\n# expect: quickshell 0.3.0 or newer\nsystemctl --user is-active graphical-session.target\n# expect: active\nsystemctl --user is-active at-spi-dbus-bus.service\n# expect: active\nbusctl --user list | grep org.a11y.Bus\n# expect: org.a11y.Bus  :1.NN  ...\n```\n\nIf any check fails, fix the underlying prerequisite before proceeding.\n\n### 2. Plugin enablement (Home-Manager)\n\nAdd the input + module as shown in [Install](#install-nixos--home-manager). Rebuild Home-Manager:\n\n```bash\nhome-manager switch\n# or, for a flake-bound HM-on-NixOS host:\nnh os switch .\n```\n\nThe rebuild output should mention:\n\n- `noctalia-appmenu-bridge` binary installed under `~/.nix-profile/bin/` (or equivalent).\n- Plugin payload at `~/.config/noctalia/plugins/noctalia-appmenu/`.\n- Systemd user unit `noctalia-appmenu-bridge.service` enabled.\n- `QT_ACCESSIBILITY=1` exported in your session env.\n\nIf you forgot to enable `services.gnome.at-spi2-core` system-wide, the HM module emits an assertion error (or `lib.warn` at evaluation) telling you which knob to set.\n\n### 3. Start the bridge + reload noctalia\n\n```bash\nsystemctl --user start noctalia-appmenu-bridge.service\nsystemctl --user status noctalia-appmenu-bridge.service\n# expect: Active: active (running)\n\nqs -c noctalia-shell ipc reload\n# OR:\nsystemctl --user restart noctalia-shell.service\n```\n\nYou should now see the appmenu slot in the noctalia topbar (initially empty when no a11y-aware app is focused).\n\n### 4. Verify with a real Qt6 app\n\n```bash\nanki \u0026\n```\n\nWithin ≤ 200 ms of Anki receiving keyboard focus, the appmenu slot renders Anki's menu strip (`File`, `Edit`, `View`, `Tools`, `Help`, `Ankimon`, `AnKing`). Clicking `File` opens a popup matching Anki's in-window menu; clicking `File → Export…` activates the action in Anki, exactly as if the in-window menu had been clicked.\n\nIf the menu does not appear:\n\n```bash\njournalctl --user -u noctalia-appmenu-bridge.service -n 100 --no-pager\n# Look for [atspi] lines: \"found app for pid\", \"fetched menubar\", \"no app for pid\", …\n```\n\n| Symptom | Diagnosis | Fix |\n|---|---|---|\n| `[atspi] no app found for pid` | App did not register on the a11y bus | Verify `QT_ACCESSIBILITY=1` is set in the app's environment: `tr '\\0' '\\n' \u003c /proc/$(pidof anki)/environ \\| grep QT_`. |\n| Bridge log shows menu fetched but bar is empty | Plugin not loaded by noctalia-shell | `qs -c noctalia-shell ipc reload`; check `journalctl --user -u noctalia-shell.service` for plugin-load errors. |\n| Submenu (`File → Open Recent`) does not open | spec 004 FR-010 regression | Capture `journalctl` output and file a bug. |\n\n### 5. Verify the release artefact (optional, recommended)\n\nAfter upgrading to `v1.0.0` (or installing from a release tarball):\n\n```bash\ngh release download v1.0.0 --repo yolo-labz/noctalia-appmenu --pattern 'noctalia-appmenu-bridge*'\ngh attestation verify ./noctalia-appmenu-bridge --owner yolo-labz\n# expect: Loaded digest sha256:...\n# expect: ✓ Verification succeeded!\n\ngh release download v1.0.0 --repo yolo-labz/noctalia-appmenu --pattern 'sbom.cdx.json'\njq '.bomFormat, .specVersion' sbom.cdx.json\n# expect: \"CycloneDX\"\n# expect: \"1.7\"\n```\n\nA second build from source should produce a byte-identical binary:\n\n```bash\nnix build github:yolo-labz/noctalia-appmenu/v1.0.0#noctalia-appmenu-bridge\nsha256sum result/bin/noctalia-appmenu-bridge ./noctalia-appmenu-bridge\n# expect: identical hashes\n```\n\n## App-menu fallback\n\nMost modern apps expose **no machine-readable menubar** on Wayland: libcosmic/Iced\n(`cosmic-files`), Electron without `--force-accessibility` (Obsidian, VS Code, Slack),\nChromium/Chrome, Firefox, and GTK4 popover-only apps all register nothing usable on\nthe AT-SPI bus. For these the bridge does **not** go blank — it serves an honest,\nidentity-derived **fallback menu** (`source = \"desktop-fallback\"` in `active.json`),\nbuilt from:\n\n- the app's freedesktop `.desktop` entry — display **Name** and any `[Desktop Action]`s\n  (e.g. Chrome's *New Window* / *New Incognito Window*, Firefox's *Profile Manager*),\n- a **New Window** launch item when the entry declares no actions,\n- a **Window** submenu of real niri controls, grouped: *Close · Toggle Fullscreen ·\n  Toggle Floating* — *Maximize Column · Center Column · Expand Column to Available\n  Width* — *Move to Previous/Next Workspace · Move to Monitor Left/Right*,\n- **Quit**, mapped to niri *close-window* (never `SIGKILL`).\n\nMenu rows carry **icons** where one is known: `.desktop` actions and the launch item\nuse the app's own `Icon` (e.g. `google-chrome`); the window controls use standard\nfreedesktop icon names (`window-close`, `view-fullscreen`, `go-down`/`go-up`, …),\nresolved by the widget via `Quickshell.iconPath`. Labels are **locale-aware** — the\n`.desktop` `Name[pt_BR]` → `Name[pt]` → `Name` chain is honoured, so an app's own\ntranslated action names appear in your language.\n\nEvery item maps to a real action — `.desktop` actions launch the app's own `Exec`\n(parsed to argv, **never** via a shell; field codes stripped), window controls call\n`niri msg action`. It is honest about *not* being the app's in-window menu: the\n`source` field says `desktop-fallback`, not `atspi`. This **supersedes** the v1.0.2\n\"honest-or-hidden\" behaviour (the bar used to collapse to nothing); see\n[ADR-0031](docs/adr/ADR-0031-desktop-fallback.md).\n\nApps that **do** expose a native menubar via AT-SPI (Qt6 / GTK with the a11y bridge\nloaded — Anki, Okular, Kate, Krita, GIMP, LibreOffice) are unaffected: they always\nget the real menu (`source = \"atspi\"`); the fallback never shadows a native menubar.\n\nTo opt back into blank-when-no-native-menu, set `desktop_fallback = false` in\n`~/.config/noctalia-appmenu-bridge/config.toml`.\n\n## Caveats\n\nKnown limitations. Each item is tracked against a follow-up spec or ADR.\n\n- **The fallback is not the app's real menu.** `desktop-fallback` surfaces launch\n  actions + window controls, not the app's File/Edit/View tree. For the real menubar\n  on Electron/Chromium/Firefox, use the per-app flags below. Native, machine-readable\n  menus are an upstream-toolkit responsibility the bridge cannot synthesise.\n- **Firefox / Thunderbird — desktop fallback on niri (by design, [ADR-0035](docs/adr/ADR-0035-lazy-reveal-locked-menubar-fallback.md)).** With `accessibility.force_disabled = 0` Firefox *does* expose a menubar over AT-SPI (`frame → tool bar \"Menu Bar\" → menu bar → File/Edit/View/…`), but its top-level menus are **lazy**: the items exist only after the menu is **visibly opened**, and AT-SPI's only action is `\"click\"`, which opens — and on niri **pins** — Firefox's own menubar. Verified 2026-06-06: a revealed menubar does not re-hide via a second `DoAction`, Escape, Alt, or focus-out. Reading the menu therefore leaves a duplicate menubar on screen, so the bridge **deliberately serves the desktop fallback for Firefox** (all-top-levels-childless ⇒ \"no readable menu\", like libcosmic #157) rather than the reveal-pinning real menu.\n\n  **The clean fix is a compositor change, not a bridge change.** Firefox ≥138 can export its menu as `com.canonical.dbusmenu` *data* (no visual reveal) — but only when the compositor advertises the `org_kde_kwin_appmenu_manager` Wayland global, which niri does not (yet). It is ~150 LoC in niri (Smithay bindings exist) and already implemented in a fork ([Naxdy/niri#46](https://github.com/Naxdy/niri/pull/46)) plus a maintainer-approved [quickshell#484](https://github.com/quickshell-mirror/quickshell/pull/484). Until niri advertises that global, **Firefox-on-niri is a documented limitation**, not a bug. (The `force_disabled = 0` pref + bridge-restart notes below still matter for the day niri gains the protocol, and for Qt/GTK apps.)\n  - **NixOS / Home-Manager:** set the pref in `programs.firefox.profiles.\u003cname\u003e.settings.\"accessibility.force_disabled\" = 0;`, **not** `about:config` — `user.js` is a HM store symlink and hand-edits to `prefs.js` are overwritten on the next rebuild.\n\n  Mozilla iterated on the Wayland a11y export through 2025–2026; the regression-free default is still partial. See [`specs/004-project-completion/research.md` §7](specs/004-project-completion/research.md) for the upstream status.\n- **Electron apps.** VS Code, Slack, Discord, etc. expose a native menubar only when launched with `--force-accessibility`; otherwise the [desktop fallback](#app-menu-fallback) applies. Wrap the launch command or set the flag in your `.desktop` file. Chromium's native AT-SPI export is \"quite good\" but flag-gated.\n- **Multi-monitor menubar duplication.** `v1.0.0` renders the focused-output menu only — no duplication across monitors. Deferred to v2 ([spec 004 §Out of scope](specs/004-project-completion/spec.md)).\n- **Alt-letter mnemonics / global Alt-F intercept.** Pressing `Alt-F` does NOT open the File menu via the appmenu. The in-window menu (if visible) still receives the keystroke. Deferred to v2 per [ADR-0010](docs/adr/ADR-0010-no-keybind-intercept-v1.md) — no clean Quickshell hook exists for global keybind interception at v1.\n- **GTK4 popover menubars.** GTK4 apps using `GtkPopoverMenuBar` (Nautilus 45+, some GNOME apps) expose menu structure only when the menu is open in-window. When the walk finds an empty menubar the bridge serves the [desktop fallback](#app-menu-fallback) instead.\n- **libcosmic / Iced apps.** System76's libcosmic toolkit (`cosmic-files`, `cosmic-edit`, `cosmic-term`, `cosmic-settings`) and standalone Iced apps have no AT-SPI implementation upstream. They register on the session bus but never join `org.a11y.atspi.Registry`, so the bridge cannot enumerate their menus and serves the [desktop fallback](#app-menu-fallback). Tracked at [#157](https://github.com/yolo-labz/noctalia-appmenu/issues/157) / [pop-os/libcosmic accessibility](https://github.com/pop-os/libcosmic/issues?q=accessibility+OR+atspi); revisit when libcosmic ships AccessKit/AT-SPI export.\n- **AT-SPI bus restart.** If `at-spi-bus-launcher` crashes and is restarted by D-Bus activation, the bridge re-flips `org.a11y.Status.IsEnabled = true` on its next focus-change attempt and resumes within ≤ 5 s. The QML widget collapses to a zero-paint stable slot during the gap (no error spam, no crash) — see [spec 004 Scenario 5](specs/004-project-completion/spec.md).\n- **niri reload.** `niri msg reload-config` may produce a ≤ 2 s blank-bar gap while the bridge reconnects; the backoff resets to its floor after any cleanly-EOF'd session ≥ 30 s, so successive reloads do not compound ([spec 004 FR-001](specs/004-project-completion/spec.md)).\n- **Compositor support.** niri is the only supported compositor at v1.0.0. Hyprland / Sway / KWin / COSMIC focus tracking is deferred to v2 ([ADR-0005](docs/adr/ADR-0005-niri-only-v1.md)); the bridge's focus-tracker abstraction door (`FocusSink` trait) is open but unwired.\n\n## Develop\n\n```bash\nnix develop                       # devShell: rust, cargo, alejandra, lefthook, gitleaks, qmllint\njust bridge.test                  # cargo test --all-features --locked\njust plugin.lint                  # qmllint (SARIF emit + upload runs in CI — FR-024)\njust integration                  # niri --headless + AT-SPI fixture end-to-end (Lane A)\n```\n\nThe bridge integration test (`bridge/tests/atspi_integration.rs`, FR-022) walks a fake AT-SPI registry stub and asserts the JSON snapshot shape end-to-end. CI runs it on every PR; locally you can run `cargo test --test atspi_integration` from `bridge/`.\n\n## Verification (release artefacts)\n\nEvery tagged release ships:\n\n- Rust bridge binary built reproducibly with `SOURCE_DATE_EPOCH`, attested via [`actions/attest-build-provenance`](https://github.com/actions/attest-build-provenance) (v4 family).\n- CycloneDX 1.7 + SPDX 2.3 SBOMs (via `syft` + [`cyclonedx-rust-cargo`](https://github.com/CycloneDX/cyclonedx-rust-cargo)).\n- GitHub-native build-provenance attestation. Verify with a single command:\n\n```bash\ngh attestation verify noctalia-appmenu-bridge --owner yolo-labz\n```\n\nSee [SECURITY.md](SECURITY.md) for the full release-engineering posture and vulnerability-reporting process.\n\n## Project layout\n\n```\nnoctalia-appmenu/\n├── plugin/                         # noctalia plugin (QML; ships to ~/.config/noctalia/plugins/)\n│   ├── manifest.json\n│   ├── BarWidget.qml\n│   └── AppmenuPopupWindow.qml\n├── bridge/                         # Rust sidecar (AT-SPI walker + fixed-proxy publisher)\n│   ├── Cargo.toml\n│   ├── src/\n│   └── tests/\n├── nix/                            # flake modules: package, devShell, HM module\n├── specs/004-project-completion/   # v0.3 → v1.0.0 roadmap (umbrella)\n├── specs/008-ci-quality-docs/      # Lane D — CI + quality gate + docs\n├── docs/adr/                       # architecture decision records (1–25)\n├── .specify/memory/constitution.md # project constitution\n├── .claude/agents/                 # specialised agents (qml-architect, dbusmenu-expert, …)\n└── .github/workflows/              # CI: ci, release, sonar, codeql, osv-scan, scorecard, reproducibility, actionlint, zizmor\n```\n\n## Acknowledgements\n\n- [Quickshell](https://quickshell.org) by `outfoxxed` — QML widget toolkit.\n- [noctalia-shell](https://github.com/noctalia-dev/noctalia-shell) — bar plugin host.\n- [at-spi2-core](https://gitlab.gnome.org/GNOME/at-spi2-core) — Linux accessibility bus.\n- [niri](https://github.com/YaLTeR/niri) by Ivan \"YaLTeR\" Molodetskikh — IPC event-stream.\n\n## License\n\n[Apache-2.0](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyolo-labz%2Fnoctalia-appmenu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyolo-labz%2Fnoctalia-appmenu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyolo-labz%2Fnoctalia-appmenu/lists"}