Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
124c960
fix(configurator): freeze draft actions during document operations
devmobasa Sep 7, 2026
0b9bca6
fix(configurator): protect unsaved drafts on reload and close
devmobasa Sep 7, 2026
ae2a486
fix(ui): fit command palette rows and navigation to viewport
devmobasa Sep 7, 2026
6676205
ci: run canonical local validation for both feature configurations
devmobasa Sep 7, 2026
a96393d
feat(toolbar): add keyboard and accessibility support to GTK sliders
devmobasa Sep 7, 2026
d1c5743
test(render): preserve and revalidate native font crash reproducer
devmobasa Sep 7, 2026
4389050
test(render): retain bounded diagnostic runner and result artifacts
devmobasa Sep 7, 2026
1508d4d
test(ui): require executed GTK widget coverage on isolated display
devmobasa Sep 7, 2026
4b4ea7e
perf(session): clone only history retained by snapshot policy
devmobasa Sep 7, 2026
fae62fc
perf(configurator): share shortcut analysis across each view refresh
devmobasa Sep 7, 2026
de652df
refactor(input): let command palette own query navigation and repeat …
devmobasa Sep 7, 2026
333ca1b
refactor(capture): derive portal lifecycle from one owned operation
devmobasa Sep 7, 2026
3558722
refactor(domain): share stroke limits without input dependencies
devmobasa Sep 7, 2026
3904c18
refactor(configurator): group history draft conversion and validation
devmobasa Sep 7, 2026
21eccf2
refactor(toolbar): share slider readouts values and event policy
devmobasa Sep 7, 2026
4fe3467
feat(ui): theme the complete command palette surface
devmobasa Sep 7, 2026
1af696b
docs: map configurator workflows and clarify Cairo rendering
devmobasa Sep 7, 2026
5585ad2
perf(render): measure sparse damage scans and retain linear replay
devmobasa Sep 7, 2026
7897a4c
perf(ui): measure thumbnail replay and document cache prerequisites
devmobasa Sep 7, 2026
dc63d27
perf(toolbar): measure unchanged snapshot construction before caching
devmobasa Sep 7, 2026
729a93a
refactor(capture): exercise cancellation through task ownership
devmobasa Sep 7, 2026
f7dbf2c
test(configurator): separate leave-flow scenarios for strict linting
devmobasa Sep 7, 2026
860795a
test(toolbar): order slider contract helper before test module
devmobasa Sep 7, 2026
c963be9
test(domain): include shared stroke limits in source inventory
devmobasa Sep 7, 2026
127f7a1
docs(render): record native stack from final concurrent validation
devmobasa Sep 7, 2026
4b12f31
docs: fold toolbar compatibility guidance into maintained guides
devmobasa Sep 7, 2026
3d9ca72
Fix review regressions and tighten state ownership
devmobasa Sep 7, 2026
e5f0d34
Fix thumbnail Spotlight sampling across page edges
devmobasa Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 70 additions & 32 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,43 +69,13 @@ jobs:
workspaces: |
.

- name: Check version metadata
run: bash tools/check-version-consistency.sh

- name: Check package repository layout
run: bash tools/test-package-repo-layout.sh

- name: Check release packaging contract
run: bash tools/test-release-packaging.sh

- name: Check dynamic gtk4-layer-shell linkage
run: |
cargo build --locked --bin wayscriber
readelf -d target/debug/wayscriber | grep -Eq 'Shared library: \[libgtk4-layer-shell\.so'

- name: Check nixpkgs recipe dependencies
run: ./tools/check-nixpkgs-recipe.py

- name: Check Rust source coverage
run: ./tools/check-rust-source-coverage.py

- name: Check process spawn sites
run: ./tools/check-process-sites.py

- name: Check config writers
run: ./tools/check-config-writers.py

- name: cargo fmt
run: cargo fmt --all -- --check

- name: cargo clippy (all features)
run: cargo clippy --workspace --all-targets --all-features -- -D warnings

- name: cargo test (all features)
run: cargo test --workspace --all-features

- name: cargo test (no default features)
run: cargo test --workspace --no-default-features
- name: Run canonical workspace validation
run: ./tools/lint-and-test.sh

- name: Check static gtk4-layer-shell linkage
env:
Expand All @@ -129,6 +99,74 @@ jobs:
path: code-health-report.txt
if-no-files-found: warn

gtk-widgets:
name: Required GTK widget contracts
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
weston \
dbus-x11 \
fonts-dejavu-core \
build-essential \
pkg-config \
clang \
cmake \
libssl-dev \
libwayland-dev \
libxkbcommon-dev \
libxkbcommon-x11-dev \
libegl1-mesa-dev \
libgles2-mesa-dev \
libdbus-1-dev \
libinput-dev \
libudev-dev \
libgtk-3-dev \
libgtk-4-dev \
libadwaita-1-dev \
meson \
ninja-build \
wayland-protocols \
libpango1.0-dev \
libcairo2-dev \
libpixman-1-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libxcb-render0-dev \
libxcb-randr0-dev \
libxcb1-dev \
libx11-dev

- name: Build gtk4-layer-shell
# Build both forms privately: normal CI exercises the dynamic source
# build used by AUR/Nix, and a dedicated step checks release linkage.
run: |
GTK4_LAYER_SHELL_PREFIX="$RUNNER_TEMP/gtk4-layer-shell" \
GTK4_LAYER_SHELL_LIBRARY_MODE=both \
bash tools/install-gtk4-layer-shell.sh
echo "PKG_CONFIG_PATH=$RUNNER_TEMP/gtk4-layer-shell/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" >> "$GITHUB_ENV"
echo "LD_LIBRARY_PATH=$RUNNER_TEMP/gtk4-layer-shell/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" >> "$GITHUB_ENV"

- name: Set up Rust
uses: dtolnay/rust-toolchain@1.98.1
with:
components: clippy, rustfmt

- name: Cache cargo builds
uses: Swatinem/rust-cache@v2
with:
workspaces: |
.

- name: Exercise widgets on an isolated display
run: ./tools/test-gtk-widgets.sh

nix-flake:
name: Nix flake
runs-on: ubuntu-latest
Expand Down
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,15 @@ not as a reason for mechanical splitting.
`wayscriber 0.9.x` and look identical.

See [tools/README.md](tools/README.md) for build, install, packaging, version, and release helpers.

Local checks and the Rust CI job both run `./tools/lint-and-test.sh`. It lints,
builds binaries, and tests the whole workspace with all features and with no
default features, alongside source and packaging checks. CI additionally checks
dynamic and static gtk4-layer-shell linkage and uploads its code-health report.

GTK widget coverage runs separately with `./tools/test-gtk-widgets.sh` (Weston and
`dbus-run-session` required). It creates a private headless display and requires
GTK initialization; an unavailable display fails this check. Successful widget
bodies print `EXECUTED` markers. Ordinary tests without a display report an
optional skip. Neither route proves layer-shell focus or screen capture behavior
on a user's compositor.
36 changes: 30 additions & 6 deletions configurator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ cd configurator
cargo run
```

The configurator renders through GTK 4 and libadwaita. It does not compile the
overlay's GPU renderer or the portal D-Bus implementation into the configurator
binary.
The configurator renders through GTK 4 and libadwaita. The overlay paints with Cairo
into Wayland shared-memory buffers. The configurator depends on the core crate with
`default-features = false`, so it does not enable the optional portal or tray runtime.

The window loads the current config, lets you tweak values across the tabbed sections, and writes
changes back through the guarded `ConfigDocument` save interface when you press Save. Loading,
Expand Down Expand Up @@ -65,7 +65,7 @@ if its UI task is no longer observed.

### Handy actions

- **Reload** – re-read `config.toml` from disk and refresh the guarded source revision. A transient load error leaves the last good document and current draft in place.
- **Reload** – asks you to Save, Discard, or Cancel if there are unsaved changes, including unfinished color or shortcut input. Closing the window uses the same choice. Save must succeed before the requested reload or close continues. Re-read `config.toml` from disk and refresh the guarded source revision. A transient load error leaves the last good document and current draft in place.
- **Configuration update available** – shown when the file's `config_revision` predates this build's keybinding defaults. The banner lists every proposed shortcut change as before → after; **Apply Update** edits the draft only, and **Dismiss** hides the offer for this run. Nothing reaches disk until you Save, and saving an unrelated setting without applying leaves both the old bindings and the old revision on disk.
- **Defaults** – drop in the built-in defaults without saving. Pressing it asks first: **Confirm Defaults** replaces the draft and **Cancel** withdraws the question, and editing anything withdraws it too. Pressing **Defaults** again changes nothing.
- **Save** – validate inputs (including numeric ranges and color arrays), merge known changes into the source TOML, and write it atomically. An existing file is backed up with a timestamp. Save is refused if the file was created, deleted, retargeted through a symlink, or changed byte-for-byte after loading; reload before retrying. If a readable file cannot be parsed, the configurator offers a warning-marked defaults-based repair draft and backs up the unreadable source before saving it. Unknown settings are retained only when the TOML structure is parseable and safely separable; malformed content remains in the backup.
Expand All @@ -78,7 +78,7 @@ if its UI task is no longer observed.
- **Default color** – toggle between named colors and custom RGB triples.
- **Keybindings** – a bulk shortcut manager over the same per-action chips, recorder, and conflict flow. Filter by All / Changed / Conflicts / Unbound / Device / Sequences, sort by category, name, or changed status, and reset visible or all keybindings with confirmation (draft-only until Save). Review Conflicts walks each collision without picking a winner. `--open keybindings/<section>?search=...` still opens that category and now selects the matching action. Press-to-bind recording covers keys, auxiliary mouse buttons, and stylus barrel buttons, plus **Record Sequence** for two- or three-chord keyboard sequences (`Ctrl+K then Ctrl+C`). Per-row reset and a raw comma-separated text editor remain available (`F5, Ctrl+K > Ctrl+C`). Super/Meta chords record when the desktop delivers them. Legacy `[tablet.stylus_button]` assignments can be moved into the keybinding list with an explicit confirmation. Source badges mark Default, Authored, Legacy Tablet, and Unavailable shortcuts.
- **Session** – persistence settings plus named-session catalog management. Rename display labels, reveal files, and forget metadata without touching files. Clear Tool State preserves boards/history while removing persisted tool defaults. Duplicate, Move, Clear Tool State, and Clear are disabled while an overlay, manually started daemon, or background service is active.
- Live dirty-state indicator plus status banner for success/error details.
- Live dirty-state indicator plus status banner for success/error details. Editing is temporarily disabled during loading and saving; failed operations restore editing and retain the draft.
- Non-fatal warnings list unrecognized config paths. Those values are preserved for forward compatibility instead of being deleted.

## Building Releases
Expand All @@ -98,7 +98,31 @@ Each workflow module owns a related set of operations:
- `app/shortcut_workflow.rs` keeps shortcut recording, text editing, and conflict resolution separate. Only one can be active at a time.
- `app/daemon_workflow.rs` manages background setup actions, status request identities, and typed feedback.

App update handlers coordinate draft changes and UI effects.
The GTK shell and refresh code live in [component/](src/app/component/); page builders in
[pages/](src/app/pages/) bind widgets to draft values and emit [messages](src/messages.rs).
[Update handlers](src/app/update/) change the model and return typed [effects](src/app/effects.rs).
[Effect execution](src/app/component/effects.rs) schedules work; [I/O](src/app/io.rs) uses
[blocking jobs](src/app/blocking_jobs.rs) for filesystem operations. Page callbacks do not write files.
[HistoryDraft](src/models/config/history.rs) and [CaptureDraft](src/models/config/capture.rs)
own their section values, conversion and validation, including intermediate text.
Shortcut summaries parse each field once per draft for an analysis pass, then reuse those values
for conflict detection, row flags and default comparisons. No parsed cache survives the refresh.

For a save, the handler validates the draft, transfers the guarded `ConfigDocument` from
`DocumentWorkflow` to `SaveConfig`, and freezes editing. The blocking job merges known settings
and performs the guarded atomic write. `ConfigSaved` returns the document on success or failure;
success establishes the clean baseline, while failure restores editing and keeps useful errors.
`DocumentWorkflow` owns the active save's validation report and optional leave continuation in
one private context. Success consumes that context once; failure discards the continuation.
A pending Reload or close continues only after success. Canceling the unsaved-changes prompt
keeps the draft; it does not attempt to cancel a durable write already in progress.

See the [application map](../docs/codebase-overview.md) for shape edits and capture, and
[CONTRIBUTING](../CONTRIBUTING.md) for setup. From the repository root, use
`cargo test -p wayscriber-configurator` for focused coverage and `./tools/lint-and-test.sh`
for the canonical workspace checks used by CI. `./tools/test-gtk-widgets.sh` runs required
GTK toolbar assertions on a private headless compositor. These checks do not prove behavior
on an installed desktop or screen-reader announcements.

Saves use `Config::validate_for_save` from the core crate.
It compares persisted typed values to detect changes outside keybindings and rejects those changes.
Expand Down
14 changes: 9 additions & 5 deletions configurator/src/app/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ pub(crate) fn run(startup: StartupRequest) {
pub(crate) struct AppWidgets {
window_title: adw::WindowTitle,
status_label: gtk::Label,
leave_actions: gtk::Box,
leave_cancel: gtk::Button,
root: adw::ApplicationWindow,
leave_previous_focus: Option<gtk::Widget>,
status_revealer: gtk::Revealer,
migration_revealer: gtk::Revealer,
migration_label: gtk::Label,
Expand Down Expand Up @@ -108,27 +112,27 @@ impl Component for ConfiguratorApp {
) -> ComponentParts<Self> {
let (model, effects) = ConfiguratorApp::new_app_with_startup(startup);
for effect in effects {
spawn_effect(effect, &sender);
spawn_effect(effect, &sender, &root);
}

let widgets = shell::build(&root, &sender);
ComponentParts { model, widgets }
}

fn update(&mut self, message: Message, sender: ComponentSender<Self>, _root: &Self::Root) {
fn update(&mut self, message: Message, sender: ComponentSender<Self>, root: &Self::Root) {
for effect in self.update_message(message) {
spawn_effect(effect, &sender);
spawn_effect(effect, &sender, root);
}
}

fn update_cmd(
&mut self,
message: Self::CommandOutput,
sender: ComponentSender<Self>,
_root: &Self::Root,
root: &Self::Root,
) {
for effect in self.update_command(message) {
spawn_effect(effect, &sender);
spawn_effect(effect, &sender, root);
}
}

Expand Down
8 changes: 7 additions & 1 deletion configurator/src/app/component/effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ use super::super::{daemon_setup, io, session_catalog};

/// Runs one effect as a Relm4 command; its result re-enters the component
/// as an ordinary message through `update_cmd`.
pub(super) fn spawn_effect(effect: Effect, sender: &ComponentSender<ConfiguratorApp>) {
pub(super) fn spawn_effect(
effect: Effect,
sender: &ComponentSender<ConfiguratorApp>,
root: &relm4::adw::ApplicationWindow,
) {
use relm4::gtk::prelude::*;
match effect {
Effect::CloseWindow => root.destroy(),
Effect::LoadConfig => sender.oneshot_command(async {
CommandMessage::ConfigLoaded(io::load_config_from_disk().await)
}),
Expand Down
33 changes: 33 additions & 0 deletions configurator/src/app/component/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ pub(super) fn build(
root: &adw::ApplicationWindow,
sender: &ComponentSender<ConfiguratorApp>,
) -> AppWidgets {
{
let sender = sender.clone();
root.connect_close_request(move |_| {
sender.input(Message::CloseRequested);
gtk::glib::Propagation::Stop
});
}
// ---- Sidebar ----------------------------------------------------
let search_entry = gtk::SearchEntry::builder()
.placeholder_text("Search settings")
Expand Down Expand Up @@ -226,8 +233,30 @@ pub(super) fn build(
));
}

let leave_actions = gtk::Box::new(gtk::Orientation::Horizontal, 6);
leave_actions.set_visible(false);
leave_actions.set_margin_start(12);
leave_actions.set_margin_bottom(6);
let leave_cancel = gtk::Button::with_label("Cancel");
for (button, message) in [
(
gtk::Button::with_label("Save changes"),
Message::LeaveSaveRequested,
),
(
gtk::Button::with_label("Discard changes"),
Message::LeaveDiscardRequested,
),
(leave_cancel.clone(), Message::LeaveCanceled),
] {
let sender = sender.clone();
button.connect_clicked(move |_| sender.input(message.clone()));
leave_actions.append(&button);
}

let content_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
content_box.append(&status_revealer);
content_box.append(&leave_actions);
content_box.append(&migration_revealer);
content_box.append(&stack);

Expand Down Expand Up @@ -291,6 +320,10 @@ pub(super) fn build(
}

AppWidgets {
leave_actions,
leave_cancel,
root: root.clone(),
leave_previous_focus: None,
window_title,
status_label,
status_revealer,
Expand Down
31 changes: 27 additions & 4 deletions configurator/src/app/component/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ use super::AppWidgets;

pub(super) fn refresh(app: &ConfiguratorApp, widgets: &mut AppWidgets) {
// Header chrome.
let subtitle = if app.is_dirty { "Unsaved changes" } else { "" };
let subtitle = if app.has_unsaved_work() {
"Unsaved changes"
} else {
""
};
if widgets.window_title.subtitle() != subtitle {
widgets.window_title.set_subtitle(subtitle);
}
Expand All @@ -18,12 +22,16 @@ pub(super) fn refresh(app: &ConfiguratorApp, widgets: &mut AppWidgets) {
let save_enabled = app.is_dirty
&& !app.document.is_saving()
&& !app.document.is_loading()
&& app.invalid_color_hex_count() == 0
&& app.shortcuts.conflict().is_none();
&& !app.has_unresolved_editor();
if widgets.save_button.is_sensitive() != save_enabled {
widgets.save_button.set_sensitive(save_enabled);
}
let busy = app.document.is_loading() || app.document.is_saving();
let busy = !app.document.allows_editing();
widgets.stack.set_sensitive(!busy);
widgets.defaults_button.set_sensitive(!busy);
widgets.defaults_confirm_button.set_sensitive(!busy);
widgets.defaults_cancel_button.set_sensitive(!busy);
widgets.migration_revealer.set_sensitive(!busy);
if widgets.reload_button.is_sensitive() == busy {
widgets.reload_button.set_sensitive(!busy);
}
Expand All @@ -50,6 +58,21 @@ pub(super) fn refresh(app: &ConfiguratorApp, widgets: &mut AppWidgets) {
widgets.defaults_button.grab_focus();
}

let leave_pending = app.pending_leave().is_some();
let leave_was_pending = widgets.leave_actions.get_visible();
if leave_pending && !leave_was_pending {
widgets.leave_previous_focus = gtk::prelude::GtkWindowExt::focus(&widgets.root);
}
set_visible(&widgets.leave_actions, leave_pending);
if leave_pending && !leave_was_pending {
widgets.leave_cancel.grab_focus();
} else if !leave_pending
&& leave_was_pending
&& let Some(previous) = widgets.leave_previous_focus.take()
{
previous.grab_focus();
}

// Status strip.
let (status_text, status_class) = match &app.status {
StatusMessage::Idle => ("", None),
Expand Down
Loading
Loading