100 Commits
Author SHA1 Message Date
devmobasa 4e35f55a16 Merge pull request #427 from devmobasa/chore/dependency-hardening
Harden dependencies and move kernel calls to rustix
2026-10-31 23:39:34 +01:00
devmobasa 5966f2d7b8 test: treat non-wake readiness as a failure in wake assertions
The old assertions required poll to report nothing, but the shared
helpers only looked for readable input, so a hangup or a bad descriptor
could satisfy a test that expects no wake. Reject any other readiness in
both helpers and pin that with a hung-up pipe.
2026-10-30 22:54:45 +01:00
devmobasa b7bfbd6df6 fix(broker): validate inherited descriptors before borrowing them
Borrowing a descriptor requires it to be open at that moment, so the
broker entry cannot check a raw number through a borrow. Check both
numbers with getsockopt first, as main did, which keeps the rejection of
closed descriptors and makes the borrow's safety claim true.

The entry exits 126 either way, so this only restores the contract. Pin
that rejection with an integration test against the real binary.
2026-10-30 22:53:29 +01:00
devmobasa 66e9259f6f refactor: finish moving kernel calls off raw libc
Detach from the terminal with rustix and std, and drop the remaining
unsafe process, poll, and shutdown calls from the tests through small
shared helpers. Prune the rustix features nothing ended up using, and
record which call sites stay on libc and why in the agent guidance.
2026-10-30 20:13:25 +01:00
devmobasa 82a8d5e9c6 refactor(broker): use rustix for descriptors and processes
Replace the hand-built sendmsg and recvmsg control messages, the sealed
memfd handling, and the polling, signalling, and child status calls with
typed rustix calls. Broker functions now take borrowed descriptors instead
of raw integers, so fd lifetimes are checked by the compiler.

The packet and descriptor caps, the truncation checks, and the signal
allow-list stay. Passed descriptors now also close when a packet with no
data is rejected.

One check narrows: rustix skips ancillary types it does not know instead
of reporting them, so only descriptors and credentials are visible here,
and credentials are still rejected. The skipped case is SCM_SECURITY,
which needs SO_PASSSEC on the socket, and nothing here sets it.

The post-clone child stub stays on raw syscalls by design, and the
broker entry keeps one borrow of its inherited channels, which live until
the process exits.
2026-10-30 20:13:25 +01:00
devmobasa c115c907ec refactor(daemon): use rustix for pidfds, timers, and inotify
Move pidfd opening and waiting, the boot clock and timerfd, the command
queue watcher, and the overlay watchdog to rustix, removing their unsafe
blocks. Deadlines, retries, and failure handling stay the same.

Three unsafe sites remain for a reason: the zero-signal pidfd check has
no rustix equivalent, the inotify parser keeps its buffer across calls,
and fail_stop exits without any cleanup. The watch mask drops two bits
the kernel ignores, because those events always arrive.
2026-10-30 20:04:02 +01:00
devmobasa f108e45964 refactor: use rustix for anchored file operations
Move the no-replace renames, the pinned runtime state paths, and the
input node access check to rustix, removing their unsafe blocks. The
paths keep their existing NUL checks and error messages.

Share one safe FIFO helper across the tests that repeated an unsafe
mkfifo block, since the std version is still unstable.
2026-10-30 20:04:02 +01:00
devmobasa b9e5f8f7fb refactor: poll and wake through rustix
Move the runtime wake eventfd, the Wayland and daemon poll loops, and the
input monitor to rustix, removing their unsafe blocks.

Timeout rounding, interruption retries, and readiness rules stay the
same. The runtime poll seam now passes readiness flags, since rustix does
not let tests write them into a poll descriptor. A test helper replaces
the repeated poll blocks in the wake tests.
2026-10-30 20:04:02 +01:00
devmobasa 572bc3ba44 refactor: take advisory locks through rustix
Add rustix as a direct dependency. It is already compiled through the
Wayland stack, so this adds no new crate.

Use its typed flock operations for the durable, daemon protocol, and
action journal locks, which removes their unsafe blocks. Deadline locks
still always retry without blocking.
2026-10-30 20:04:02 +01:00
devmobasa d3b67cae56 refactor(cli): tokenize arguments with lexopt
Replace the hand-written tokenizer and its short-cluster and attached
value helpers. Every accepted form and value rule stays the same, now
pinned by tests, including that a value starting with - must be attached.

Two edges change: a bare -- now ends options as usual, and an unknown -x
reports as an unknown short option like one inside a cluster. Non-UTF-8
session paths now reach the specific error for that case instead of the
generic one; they are still rejected.
2026-10-30 20:04:02 +01:00
devmobasa 1dafb9385b docs: add nextest as the fast local test run
The local gate serializes tests because of a native font race. nextest
gives each test its own process, so the whole suite can run in parallel
while iterating. It is local only: it skips doctests and the isolated
render tests, so the gate stays as it is. Add it to the Nix shell.
2026-10-30 19:25:15 +01:00
devmobasa 8d0c13f946 ci: add weekly dependabot updates
Propose Cargo and GitHub Actions updates, grouping compatible Cargo
bumps. Hold zbus at its current major while ksni requires zbus 5.
2026-10-30 19:25:05 +01:00
devmobasa e009114d63 ci: enforce a dependency policy with cargo-deny
Add deny.toml and a dependency-policy job covering the whole workspace.
Bans, licenses, and sources block; advisories report without blocking,
so a newly published advisory cannot fail an unrelated pull request.

zbus 6 absorbed zvariant, zvariant_derive, and zbus_names, so RustSec
now lists them as unmaintained. ksni still requires zbus 5, so those
advisories are ignored with that reason until it moves.

Both packages now state their MIT license instead of leaving the check
to detect it. The configurator is marked publish = false: it has an
unversioned path dependency and cannot be published.
2026-10-30 19:25:05 +01:00
devmobasa 837d2d2d43 chore(deps): use one random source on getrandom 0.4
Bump the crate to the version tempfile already compiles, which removes
the duplicate 0.3 build. Make it required and replace the hand-written
libc getrandom loop in the daemon protocol, so portal tokens and protocol
identifiers share one source and one unsafe block goes away.
2026-10-30 19:21:16 +01:00
devmobasa 283c90a562 chore(release): bump version to 0.9.28 2026-10-27 10:14:18 +01:00
devmobasa 0cd97c313c chore: update Rust toolchain to 1.99.1 2026-10-27 10:13:17 +01:00
devmobasa 297a84e7a8 chore(deps): update compatible Cargo dependencies 2026-10-27 10:13:17 +01:00
devmobasa 908d12b763 Merge pull request #426 from devmobasa/style/pedantic-lints
Enable pedantic Clippy lints one at a time
2026-10-26 18:18:31 +01:00
devmobasa 46302a300a docs(agents): describe the newly enforced Rust style rules
State the rules behind the pedantic lints in plain terms, and note that
pedantic lints are enabled one at a time, never as the whole group.
2026-10-26 14:31:40 +01:00
devmobasa 86894088f2 style: enable three pedantic lints fixed by hand
comparison_chain, ignore_without_reason, and
unnecessary_debug_formatting.

- The one-edit keybinding comparison matches on `a.len().cmp(&b.len())`.
- The Wayland smoke test says why it is ignored.
- The configurator search text includes session paths with `display()`
  instead of `{:?}`, so searches match the path without debug quotes or
  escapes.
2026-10-26 14:31:40 +01:00
devmobasa ede5426ec7 style: enable the elided_lifetimes_in_paths rustc lint
Write the anonymous lifetime in paths: `BindingInserter<'_>`, not
`BindingInserter`, so a borrowed type is visible in the signature. The
lint goes in a new `[workspace.lints.rust]` table.

Applied from the compiler's suggestions, then `cargo fmt --all`.
2026-10-26 14:31:40 +01:00
devmobasa 44dba8de2a style: enable clippy::manual_let_else
Use `let ... else` for a `match` or `if let` that only unwraps a value
when the other branch diverges, for example with `return`, `continue`,
or `panic!`.

Clippy's suggestions for this lint are not machine-applicable, so they
were applied and each site was read. One suggestion lost a binding name
in the context-menu focus test; that site was fixed by hand. Every
`panic!` and `bail!` in the new `else` blocks ends with a semicolon.
2026-10-26 14:31:40 +01:00
devmobasa 0be2ab4353 style: enable nine small pedantic lints
duration_suboptimal_units, elidable_lifetime_names, explicit_iter_loop,
ignored_unit_patterns, implicit_clone, manual_is_variant_and,
manual_string_new, option_as_ref_cloned, and redundant_else.

Mechanical, with the lints enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all

The tray shutdown calls return `()`, so they are plain `.await;` rather
than the `let () = ...` that the unit-pattern fix writes.
2026-10-26 14:31:40 +01:00
devmobasa 12e81922d7 style: enable clippy::needless_raw_string_hashes
Give a raw string only the `#` delimiters its content needs: `r"..."`
when it contains no `"`, and `r#"..."#` when it contains `"` but not
`"#`.

Mechanical, with the lint enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all
2026-10-26 14:31:40 +01:00
devmobasa dba123b843 style: enable clippy::if_not_else
Put the positive condition first: `if x { a } else { b }` instead of
`if !x { b } else { a }`.

Mechanical, with the lint enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all
2026-10-26 12:46:35 +01:00
devmobasa 26937f03eb style: enable clippy::manual_midpoint
Use `midpoint` instead of `(a + b) / 2`. The result is the same
wherever the sum does not overflow. Integer `midpoint` rounds toward
zero like `/ 2`. `f64::midpoint` computes `(a + b) * 0.5` when both
inputs are at most `f64::MAX / 2` in magnitude. Only where the sum
would overflow does `midpoint` differ: it still returns a value
between the two inputs.

Mechanical, with the lint enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all
2026-10-26 12:46:35 +01:00
devmobasa 105de143fe style: enable clippy::unnested_or_patterns
Nest or-patterns inside the shared constructor:
`Some(A | B)` instead of `Some(A) | Some(B)`.

Mechanical, with the lint enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all
2026-10-26 12:46:35 +01:00
devmobasa eac0079e57 style: enable clippy::borrow_as_ptr and ptr_cast_constness
Take raw pointers with `&raw const` and `&raw mut` instead of casting a
reference, and change pointer constness with `cast_mut()`. The FFI calls
receive the same pointers, without an intermediate reference.

Mechanical, with the lints enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all
2026-10-26 12:46:35 +01:00
devmobasa d685951430 style: enable clippy::single_match_else
Write a `match` with one pattern and a fallback as `if let ... else`.

Mostly mechanical, with the lint enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all

Four matches with comments in their arms were converted by hand, so the
comments stay with their branch: config edit completion, the properties
toggle knob, a toolbar preset-slot test, and the configurator caret
restore.
2026-10-26 12:46:35 +01:00
devmobasa 9baeb9fdeb style: enable clippy::semicolon_if_nothing_returned
End unit-valued trailing expressions with a semicolon.

Mechanical, with the lint enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all
2026-10-26 12:46:35 +01:00
devmobasa 2b21ff1d80 style: enable clippy::doc_markdown
Put identifiers and Wayland protocol names in doc comments in backticks
(`wl_output`, `zwp_tablet_v2`). Product names that docs write plainly
(GLib, HarfBuzz, HiDPI, JetBrains, MSBuild, StatusNotifierItem, VSync)
go in `doc-valid-idents` in clippy.toml, next to Clippy's default list.

Mechanical, with the lint enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all

Two comments that the fix could not mark up cleanly were reworded by
hand: the surface module summary and the zoom-chip invariant.
2026-10-26 12:46:35 +01:00
devmobasa 9c4336c645 style: enable clippy::map_unwrap_or
Use `map_or`, `map_or_else`, `is_some_and`, and `is_ok_and` instead of
`map(..).unwrap_or(..)`.

Mostly mechanical, with the lint enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all

Written by hand:
- The four caret-movement arms in caret_edit.rs are a `match`. The
  `map_or_else` form needs `caret` and `anchor` mutably in both
  closures.
- `sub_ring_children` is a `match`: the `&[]` default does not infer
  as a slice in `map_or`.
- The configurator's recorded-device mapping matches on
  `Option<InputSource>` directly, instead of a `map_or` with a nested
  `match`.

The translations guard lists the `tr(...)` arguments it accepts without
a literal as source text, so its two action-metadata entries follow the
new spelling.
2026-10-26 12:46:35 +01:00
devmobasa 85d0c9804b style: enable clippy::uninlined_format_args
Inline named arguments in format strings: `format!("{name}: {err}")`.

Mechanical, with the lint enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all
2026-10-26 12:46:35 +01:00
devmobasa 10f743ac79 style: enable clippy::cast_lossless
Convert numbers that always fit with `From` (`f64::from(width)`) instead
of `as`. The lint skips `const` initializers, where `From` cannot be
called, and conversions to `usize` and `isize`, whose width depends on
the target, so those keep `as`.

Mechanical, with the lint enabled in Cargo.toml:

    cargo clippy --fix --workspace --all-targets --all-features
    cargo clippy --fix --workspace --all-targets --no-default-features
    cargo fmt --all
2026-10-26 12:46:35 +01:00
devmobasa abb03bb6bf Merge pull request #425 from gsmith-alvarez/fix/unconstrained-panned-selection-move
fix(input): allow unconstrained selection translation on pannable boards and sync drag anchor
2026-10-26 08:58:11 +01:00
devmobasa da68d152ac fix(input): keep selection movement stable across canvas bounds
Keep the press as the drag anchor until the pointer leaves the click
radius, so the first drag motion is not lost. After that the anchor
follows the pointer on every motion, including blocked ones, so
reversing at an edge moves the selection at once.

Apply coordinate limits on every board. Pannable boards still move
freely, but each translated coordinate and the painted bounds now stay
in the i32 range. Before, a nudge near i32::MAX wrapped in release
builds: the line jumped to span the whole coordinate range and was
saved that way. Debug builds panicked instead.

On a fixed canvas (the transparent overlay, or a board with panning
disabled), a selection that already crosses an edge, or lies outside
the canvas, can no longer move further out, and it is never pulled
back. Before, any nudge snapped it inside, up to thousands of pixels.

Align the move-to-edge actions with the visible canvas, including pan
and zoom, instead of the screen origin. A target further away than
i32::MAX is reached in several steps that record one undo entry.

Combine selection bounds in i64. Shape bounds are clamped to the i32
range, so a shape at the coordinate limit is measured on a copy moved
toward the origin; the limits and edge actions then see its full
stroke padding. A selection whose painted bounds end past i32::MAX
gets no selection Rect, because handle hit-testing and other selection
code compute x + width in i32. Its chrome then marks full damage, so
the halos of its other members are still repainted. The Properties
panel uses the same bounds, the text resize handle is omitted when it
would end past i32::MAX, and handle hit-testing compares pointer
distances in i64.

Test changes:
- nudge_selection_clamps_right_and_bottom_edges starts at (87, 87).
  At (90, 90) the stroke padding already crossed the canvas edge, and
  the test passed only because the old clamp pulled the shape back.
- moving_a_selection_on_transparent_board_clamps_to_screen checks the
  exact landing edge. It also passed when nothing moved.

The coordinate guard and the view-relative edge targets are adapted
from #424.
2026-10-25 21:34:40 +01:00
devmobasa f1bf4841a4 Merge pull request #423 from gsmith-alvarez/fix/issue-422-selection-move
fix(input): clamp selection translation against visible canvas rect (#422)
2026-10-25 16:42:00 +01:00
devmobasa 4c0981a336 Merge pull request #421 from devmobasa/refactor/cleanup-style
refactor: standardize Rust style across the workspace
2026-10-22 10:25:25 +02:00
devmobasa d57ad6094e test(gtk): wait for stable capture reference pixels
Several identical frames can precede a popup's final layout, leaving the
capture restoration test with a premature reference image. Require a
quiet interval before recording that image while retaining the existing
settling deadline and exact restoration assertions.
2026-10-22 10:04:07 +02:00
devmobasa 87e1d057f7 refactor: standardize Rust style across the workspace 2026-10-22 09:26:12 +02:00
devmobasa c6e89d0072 docs(i18n): point the Chinese README sync marker at the restructure
README.zh-CN.md already mirrors the English README at e2772757, so the next release sync can start its diff there.
2026-10-21 17:06:16 +02:00
devmobasa e2772757e9 docs(readme): restructure the README as a guided tour
Lead with platform support, then walk through the features with the
current screenshots and demo videos beside them. Each feature section
opens with the keys to press, keeps a short list visible, and moves
the long details into collapsed sections. The reference sections
fold their deep material the same way.

Remove the duplicated feature list from the roadmap and the repeated
help shortcuts. Split code blocks so each copy button copies one
runnable command or one sequence, and keep installer dry runs apart
from the install itself.

Mirror the new structure in the Simplified Chinese README.
2026-10-21 17:06:11 +02:00
devmobasa c9d1143e2f Merge pull request #420 from devmobasa/chore/dotnet-11-rc2
Update the tooling SDK to .NET 11 RC2
2026-10-19 12:19:07 +02:00
devmobasa 099cfea832 chore(tooling): update the pinned .NET SDK to 11 RC2
Update global.json and the SDK consistency guard to
11.0.100-rc.2.26504.105. Refresh the release and version-metadata fixtures
while preserving strict SDK selection.
2026-10-19 12:05:49 +02:00
devmobasa 7950b621c9 Merge pull request #419 from devmobasa/feat/findable-language-row
Make the configurator language setting easier to find
2026-10-19 12:02:46 +02:00
devmobasa 70d2e70f44 feat(configurator): make the language row easy to recognize
Put the language picker first in General UI, with a globe and a title
that includes every supported language. Derive its title and search terms
from shared language metadata.

Add regression coverage for the title, icon and row order, and format the
additions with the pinned Rust toolchain.
2026-10-19 11:39:54 +02:00
devmobasa ce68c7d893 Merge pull request #418 from devmobasa/fix/417-gtk-capture-transparency
Fix GTK toolbar artifacts in screen captures
2026-10-19 00:58:54 +02:00
devmobasa 932ac6d019 test(gtk): remove compositor-dependent constrained menu case
The menu-constrained case needs a compositor that shrinks an oversized
popup. Weston 13 on the Ubuntu 24.04 CI runner does not shrink it, so the
menu never overflows and the case fails before capture.
2026-10-19 00:40:59 +02:00
devmobasa c22f672d87 fix(gtk): prevent toolbar artifacts in screen captures 2026-10-19 00:30:14 +02:00
devmobasa 0d855869ff chore(release): bump version to 0.9.27 2026-10-18 12:25:20 +02:00
devmobasa f231695d48 docs(i18n): synchronize the Chinese README for 0.9.27 2026-10-18 12:25:20 +02:00
devmobasa 5f3a257f88 fix(runtime): use try_update for controller IDs 2026-10-18 10:41:56 +02:00
devmobasa dcaf924420 Merge pull request #416 from devmobasa/fix/untranslated-config-values
Keep saved font and color values independent of the UI language
2026-10-18 10:36:54 +02:00
devmobasa 00134fb415 Merge pull request #415 from devmobasa/docs/chinese-readme-and-desktop-entries
Add the Chinese README and localized desktop entries
2026-10-18 10:16:53 +02:00
devmobasa d9165ca50a fix(configurator): keep font and color config values untranslated 2026-10-18 10:15:24 +02:00
devmobasa 3eae01f96c packaging: localize desktop entries for Simplified Chinese 2026-10-18 09:20:00 +02:00
devmobasa 7ebaf43a4d docs(i18n): add the complete Simplified Chinese README 2026-10-18 09:20:00 +02:00
devmobasa 3177c5529b docs(i18n): expose language choices and document README synchronization 2026-10-18 09:14:01 +02:00
devmobasa 6342495c02 Merge pull request #414 from devmobasa/feat/language-command-search
Improve language discovery, translated search, and Chinese shortcuts
2026-10-17 22:37:06 +02:00
devmobasa d3f583e15a feat(i18n): search translated command text without changing English scores 2026-10-17 21:42:09 +02:00
devmobasa afbdbd83b9 feat(i18n): make the language setting easy to find 2026-10-17 21:42:09 +02:00
devmobasa b972b32462 fix(i18n): keep service labels and recorded keys unambiguous 2026-10-17 21:41:48 +02:00
devmobasa c907eef09b Merge pull request #411 from weimona/main
Add multi-language UI support (zh-CN first)  「using deepseek-V4.1-flash」
2026-10-17 17:28:49 +02:00
devmobasa fcbc437816 feat(cli): choose and list the interface language from the command line
`--language CODE` sets the interface language for one run of --active,
--freeze, --daemon or --about without changing config.toml. It accepts
the spellings `[ui] language` does and takes precedence over both
WAYSCRIBER_LANGUAGE and the config. A daemon started with it gives it to
the About windows it opens, and to its overlays as an optional
environment input that an older overlay ignores. The process broker
strips that input from every other helper, so a configurator opened from
an overlay keeps previewing its own Language row. With --daemon-toggle or
an overlay action the flag is an error, since a running daemon's
overlays cannot be changed from outside.

`--list-languages` prints each language's code and name, the configured
language, and the language in effect with the setting that decided it.

Languages now carry their native and English names, and the
configurator's Language row takes its labels from them. The help text,
README, CONFIG.md, I18N.md and config.example.toml describe both options.
2026-10-16 18:42:33 +02:00
devmobasa 723436c739 Merge pull request #413 from devmobasa/fix/412-redundant-icon-theme-path
Stop advertising system icon roots as the tray icon theme path
2026-10-16 18:27:58 +02:00
devmobasa 38fea7c91f Stop advertising system icon roots as the tray icon theme path
The tray advertised <prefix>/share/icons as its IconThemePath, so a
package installed under /usr reported /usr/share/icons. Tray hosts
already search that directory, and some index the advertised path
recursively: Noctalia walks every installed icon theme each time its
tray drawer opens (noctalia-dev/noctalia#4338).

- Leave the installed path out when it is the icons directory of a data
  directory a spec-following host searches: the XDG data home and the
  system data directories, with the spec defaults and with relative
  values ignored. Custom prefixes such as /opt and Nix store paths keep
  advertising their own icons.
- Advertise the installed path only while the tray names its symbolic
  icon. Colored pixmaps need no theme path.
- WAYSCRIBER_ICON_THEME_PATH still overrides the path, and an empty
  value advertises none. docs/CONFIG.md now documents both.

Refs #412
2026-10-16 18:10:19 +02:00
devmobasa 98b6f54c15 docs(i18n): describe per-process languages, previews and override limits
docs/I18N.md still said the configurator was untranslated, that the
language was set once at startup, and that a translation could reorder
the {} placeholders; the guard rules and the steps for adding a language
were incomplete. It now covers which processes draw translated text and
when each picks up a change, the configurator preview, how auto resolves
in a systemd user service, the fixed placeholder order, the guard's call
forms and its derived-label audit, the limits on local catalog overrides,
search under translation, and every place a new language has to be added.

CONFIG.md, config.example.toml and the catalogs README state the same
timing, override and placeholder rules.
2026-10-16 18:04:08 +02:00
devmobasa 89ffe56e81 fix(configurator): translate the remaining configurator strings
Much of the configurator still drew English in Chinese: row titles and
hints, buttons, accessibility labels, placeholders, validation errors,
status messages such as the shortcut reset and conflict-review notices,
the session card's Created, Opened and Saved prefixes, and the
background-setup status lines. They are now marked where they are drawn,
with format! text turned into trf templates and displayed constants
written as literals so the catalog guard can read them, and the Chinese
catalog gains an entry for each new key.

Values written into config or other programs stay as they were: default
board and profile names, the GNOME shortcut name and default quick-color
labels.
2026-10-16 18:04:01 +02:00
devmobasa b0cd56b7a2 feat(configurator): preview a language across the whole window
Picking a language set the process language, but every label already on
screen kept its old text, so only dialogs opened afterwards changed. A
reload, Defaults or a finished save replaced the draft without applying
its language, which left the window in the language of the previous
draft. Startup read the config a second time, synchronously, just to pick
the language before the window was built.

The draft's language is now applied on every transition that replaces
the draft, and by the Language row, before the status text for that
transition is built. When the language changes, the shell rebuilds its
pages from the model and retitles the persistent chrome in place. The
model owns everything typed or selected, so nothing entered is lost; the
rebuild carries focus, the caret and selection, and the visible page's
scroll position across, and editor popovers closed by the rebuild no
longer cancel their edits. The window opens in English and switches when
the first load arrives, and that load job prepares every catalog off the
GTK thread.

Text the configurator stored for display was translated once and kept:
the "Never" of a catalog entry, the shortcut recorder's prompt, the
claimants of a pending shortcut conflict. Those now store what they
report and are rendered when drawn. The language list names each
language in itself, so a preview the user cannot read can still be undone,
and search matches the English terms in every language as well as tab,
sub-tab and section titles as displayed.

A GTK test runs the real window on the private headless compositor and
checks a language switch with open editors, focus, caret, scroll and
navigation; both GTK gates require its marker.
2026-10-16 18:03:00 +02:00
devmobasa 996d4f1f0b fix(i18n): read every marked call in the translation guard
The catalog guard read only a string literal that followed tr( or trf( on
the same line, so the calls rustfmt wraps onto the next line were never
checked, and a call whose argument was a constant or an expression was
skipped without a word. Three strings had no catalog entry as a result:
"Light Mode drawing", "Light Mode passthrough" and "Path copied". The
guard compared only the {} counts in the catalog, so a template spelling
{min} and {max} passed while trf left those words in the message, as the
whole-number range error did.

The guard now reads plain and raw literals wherever they open the call,
across lines, and trf(tr("…"), &[…]). Any other argument fails, except in
the action accessors, whose metadata the guard audits directly, now
including the compact labels action_display_label derives. A trf template
must spell only {} and pass as many values as it has placeholders.

The three calls now spell their literals and have Chinese entries, the
range message uses two {} slots, and the trf documentation states that
values fill placeholders in order, so a translation can move them but not
swap two.
2026-10-16 18:00:43 +02:00
devmobasa 04e4e43d67 fix(i18n): select the saved language in background mode and the About window
The About window is its own process, started with --about, and read the
config only for its theme, so its text stayed English whatever
[ui] language said. The daemon never selected a language either, so its
tray menu, portal shortcut descriptions and update notices stayed
English; an overlay it starts is a separate process and cannot set the
daemon's language.

i18n::initialize() selects a language and loads its catalog before the
first surface is built. The overlay calls it where it already selected
its language, the About window calls it with the language from the config
read it makes for its theme, and the daemon reads the saved language once,
before its tray, shortcut and update-notice workers start, whichever are
enabled. A config the daemon cannot read keeps the automatic default.
Background mode picks up a change when it restarts.
2026-10-16 17:59:22 +02:00
devmobasa 2826727ac7 fix(i18n): bound local catalog overrides and keep one toolbar table per language
A local catalog override was read with read_to_string on the first
translated lookup: a FIFO named zh-CN.toml blocked that lookup until a
writer appeared, a large file was read whole, and a symlink was followed.
Overrides are now opened without following symlinks and without
blocking, checked on the opened descriptor to be a regular file, and read
up to 1 MiB. A file that fails those checks, or is not UTF-8 or TOML, is
ignored with one log message and the embedded catalog is kept; an entry
whose {} placeholders differ from its key is skipped. prepare_catalogs()
loads the catalogs ahead of the first lookup for processes that want that
file work done early.

The toolbar definitions cached only the last language and leaked a new
table with Box::leak on every switch. Each language now has one table,
built once from that language through i18n::lookup, so a table never
mixes two languages and switching back reuses it.
2026-10-16 17:57:27 +02:00
devmobasa f6225ca1c6 fix(i18n): keep identifiers and control flow off translated text
Several places used displayed text as an identifier or read it to decide
what to do, so they broke once that text was translated:

- The configurator named its GtkStack pages after their translated tab
  titles. The names were registered in the startup language and looked up
  in the current one, so after a language change the sidebar and the UI
  sub-tabs stopped switching pages. Tabs now have fixed stack names.
- The arrow-style menu built its widget IDs from the translated style
  label, and the layout menu from the translated preset label, in both
  toolbars ("top.arrow-style.pointy" became "top.arrow-style.尖头"). Both
  now derive the ID from the typed value.
- The session catalog cleared its loading notice only when the status
  contained the English "Loading sessions", which never matched in
  Chinese. The notice is now a typed status that completion recognizes in
  any language, and that re-renders when the language changes.
- The background-setup panel chose its failure styling by searching the
  feedback for "failed" or "error", so translated failures looked like
  successes. The feedback is now typed and carries its outcome, including
  a light-control setup whose Hyprland reload failed after the controls
  were written.
- The eraser tooltip compared the slider label with a translation; it now
  reads a typed flag.
2026-10-16 17:55:38 +02:00
devmobasa d506fa2714 test(i18n): keep language tests independent of the runner's locale
The catalogs load once per process, with whatever override file the
runner has installed, and WAYSCRIBER_LANGUAGE beats every configured
language, so a test that asserts Chinese text, or one that selects
English, depended on the machine running it.

Core tests that assert embedded Chinese now run in a fresh copy of the
test binary with the override, the locale variables and the override
directory cleared. The i18n integration test points the override
directory at an empty one. with_temp_config_home clears the override for
the config it loads and restores the process language afterwards, since
loading a config at startup selects its language.

The configurator, which links the core without its test-only locale
neutrality, clears those variables once per test process, before any
test takes the environment lock, and gains the same child runner for
tests that select a language other than English.
2026-10-16 17:52:53 +02:00
devmobasa 70fc8fbda7 style(i18n): format the language support and fix its lint warnings
The language-support commit left 108 files unformatted, so the format
check failed, and strict Clippy stopped on a collapsible if in the toolbar
definitions, needless lifetimes on the catalog lookup, and two needless
borrows in the shortcut recorder prompts. This is rustfmt's output plus
those four mechanical fixes; behavior is unchanged.
2026-10-16 17:49:03 +02:00
devmobasa 22dc33ee69 Change demo video link in README
Updated demo video link in README.md
2026-10-13 19:54:59 +02:00
devmobasa 584b94cb3a Fix README demo link and hero image description
The demo video moved to /media/demo.webm on wayscriber.com, so the old
link returned 404. Describe the current hero screenshot in its alt text.
2026-10-13 19:52:59 +02:00
devmobasa 7ac885f9d2 Keep fractional pen ink and add direct zero-level meter dots
Pen, Marker and Shape Pen strokes now keep fractional pointer positions
from press to commit instead of rounding every sample to whole canvas
units, which left visible stairs on fractional-scale outputs.

- Freehand, pressure and marker points are stored as f64. Motion samples
  closer than 0.75 canvas units to the last kept sample are skipped, the
  release position is always kept, and a skipped tablet sample still
  raises the last kept width to its pressure peak.
- Smoothing runs in floating point with pinned endpoints. Smoothed
  interior points are committed on a 1/1000 canvas-unit grid to keep
  sessions compact.
- Session format 8 stores fractional points and validates them on save
  and load. Integer points from older sessions load exactly.
- Shape recognition, laser and eraser geometry keep integer paths
  through one rounding adapter.
- Level meters and steppers gain an accent dot that sets Off (or Precise
  for shape detection) in one click, and each bar now selects its exact
  level. Built-in and GTK toolbars share the dot painter, and meter rows
  widen by the dot slot so bar widths are unchanged.

Validated with ./tools/lint-and-test.sh and tools/test-gtk-widgets.sh.
2026-10-10 21:22:06 +02:00
devmobasa 91559a5ce9 Merge pull request #410 from devmobasa/fix/398-unknown-output-preflight
Fix capture preflight when the surface output arrives late
2026-10-09 16:04:42 +02:00
devmobasa 8459442fdf Fix capture preflight during late output discovery 2026-10-09 15:38:24 +02:00
devmobasa 7e113d1e69 Merge pull request #409 from devmobasa/fix/october-session-input-followups
Harden session recovery, input handling, and persistence
2026-10-06 00:22:38 +02:00
devmobasa a9e324b156 Harden session recovery, input handling, and persistence 2026-10-06 00:02:42 +02:00
devmobasa fc4c9c7614 Merge pull request #408 from devmobasa/feat/daemon-session-continuity
feat(session): keep the active session across daemon hide/show
2026-10-05 02:24:42 +02:00
devmobasa e190a47d67 docs: state when session reports are read and what a remembered file loses
The protocol doc said reports are read only at retirement and removed
either way. It now says the visible-target guard also reads the current
report, and that nothing in a report directory that is not private is read
or removed. CONFIG.md says that a moved or deleted remembered file opens
home even when a backup or recovery copy is left beside it, unlike a
startup --session-file, and its session command list renders as one tight
list again.
2026-10-05 02:02:49 +02:00
devmobasa f4adda54db refactor(session): tighten the continuity code from the re-review
- An output's session load is now an enum, Loaded or WentHome, so the
  impossible state of neither a loaded session nor a reason is gone.
- The daemon checks only the shape of a reported session path. A file that
  changes before the next show is the overlay's to judge when it
  continues it; checking it at retirement kept a stale remembered session.
- The report file name is built in one place.
- The shared private-directory helper again refuses a directory another
  user owns.
- launched_resume_policy names the environment's own policy apart from
  resume_override_from_env; both now say how they differ.
- Return home asserts that a home too large to load never reaches the
  canvas, instead of dropping the value.
- The fake helper keeps launch arguments as bytes, so an argument that is
  not UTF-8 cannot stop a helper start from recognising itself.
- Imports sit at the top of the action journal and command layout, and
  docs no longer say a remembered session is continued "if it still
  exists".
2026-10-05 02:02:49 +02:00
devmobasa 1669ba6b08 fix(session): hold home and retried sessions to files a save can replace
Return home checked only a named home before loading it. A configured home
whose session file had become a symlink was followed, and one that had
become a directory loaded from a recovery copy beside it; either replaced
the current canvas and committed home, and every later save failed. Home
now loads only while its file is one a save can replace, a regular file or
none yet, checked before and after the load.

When the first load of a remembered session failed and was retried, the
output transition saved the current session before loading, and that
session was still the never-loaded remembered one. If its file had been
deleted with nothing left beside it, the save recreated it, the load then
accepted it, and the overlay continued an empty session at the deleted
path; with its folder gone too, every retry failed on the save and never
reached home. The transition now checks the remembered session before that
save and skips saving one that can no longer be used, so the load gives it
up for home.
2026-10-05 01:58:51 +02:00
devmobasa abd28c9eb1 test: read the launch arguments of helper starts in one place
The fake overlay kept its own reader of /proc/self/cmdline beside the one
the fake helper constructor uses. It now reuses that reader. The helper's
header no longer claims that no test relies on a system program: it is the
helpers it plays that need neither a script nor one.
2026-10-05 01:34:17 +02:00
devmobasa e3c20f856e docs: describe session reports and keep the session list whole
The hide-and-show paragraph in CONFIG.md had landed inside the list of
session CLI helpers, cutting the last helper off from the list, and its
unquoted <session> was eaten as an HTML tag. It now follows the list, quotes
the button names as code, mentions a remembered file replaced by something
other than a regular file, and states the --no-resume-session decision: a
daemon started with it still continues a named session the overlay switched
to, since a named session file always persists, while its default home
stays unsaved.

The v2 protocol doc now describes the optional launch variables, the
overlay-targets/ reports, when the daemon reads them, and why leftovers do
not reach an older daemon. The codebase overview credits the report writer
and reader to the files that hold them.

The no-Python guard reads the first line of every file for a Python
shebang, including file types it otherwise checks only by name; its header
and the tools README now say so.
2026-10-05 01:34:17 +02:00
devmobasa 7aba636e8c fix(daemon): harden reading and removing overlay session reports
Reports are read and removed only through a report directory that is a
real directory private to this user, so a symlinked overlay-targets/ is
neither read through nor cleaned through. A reported session file must
also pass the shape rules every named session file follows, not only be
absolute.

When a stop forces the child down because its broker failed, the child
was still retired and its final report read; the report now travels with
the error to the daemon instead of being dropped.

The private-file reader and the private-directory helper are now shared:
the report reader uses the bounded protocol reader with a privacy check,
and the command layout, the action journal and the reports create their
directories through one helper. The daemon reads a trusted report through
one function that logs and ignores what it cannot trust.
2026-10-05 01:31:15 +02:00
devmobasa 7654b24285 fix(session): keep the current session when home cannot load
Return home handed what the load found to the startup handler, which
starts on an empty canvas for a session that is not a regular file or one
too large to restore. A home that had become a directory, or had grown
past the load cap, cleared the current canvas and history, switched
target and was reported to the daemon as home.

Home now commits inside the session transaction, through the same
launch-style outcome handling the startup load uses, now shared, so the
transaction's guards cover the whole switch. Those two outcomes fail the
command and leave the current session as it was, and a named home is
checked like a startup session file before it loads. A home without
persistence is applied in the transaction too.

After home commits, the overlay rechecks its output: a per-output home
follows an output change that happened while it loaded. The Session menu
offers the way home only while a persisted session is active, since
without one the overlay is in a home it cannot leave, and the row goes
through the same item table as the other session controls.
2026-10-05 01:25:13 +02:00
devmobasa c25605d2b8 fix(session): settle a remembered session in the load that uses it
The first load checked once, ahead of loading, whether any artifact of the
remembered session existed, and then loaded it with startup rules. A file
moved away came back from the backup left beside it; one replaced by a
symlink or directory opened as an empty canvas whose saves then failed; and
a file deleted after the check, or before a retry of a failed load, became
a new empty session at the missing path.

A remembered session now loads through its own worker operation, which
holds it to the runtime Open checks before and after the load: the file
itself must be a usable regular session file, not merely a backup or
recovery copy. Every attempt until a session has loaded runs that
operation, including retries. When the file cannot be used, home loads in
its place in the same attempt; the notice and the report to the daemon
follow once home has committed.

A continued remembered session is reported as the overlay starts, before
the daemon sees it ready. The daemon launched it at home, so until now its
visible-target guard took the overlay for being at home. A report that
could not be written is tried again on the next commit instead of being
treated as delivered.

Home's options in a daemon launch follow the resume policy the daemon
passed. The run's own override is the one its --session-file forces on and
says nothing about the default session, so a daemon started with
--no-resume-session no longer gets a persistent default home.
2026-10-05 01:17:12 +02:00
devmobasa d5baaaddce feat(session): return to the home session from the Session menu
The Session popover gains a way back to the home session: "Back to
<session>" when the overlay or its daemon started with a session file, or
"Default session" without one. It is disabled while home is already
active, and the side.session.home toolbar item hides it. The label and
destination come from the launch's home input, not from the session the
overlay is in.

Returning home is an explicit session command. Like Open, it saves dirty
current data first, refuses if the canvas or interaction changes while it
is pending, and replaces nothing unless home loads. Home loads the way a
launch would, from its own options for the current output, so recovery,
clear-boundary and tool-restore rules apply, and its load errors stay
those of a startup session file rather than becoming an empty session. A
home with persistence disabled continues unsaved on an empty canvas.

Once home is committed the overlay reports it, so the daemon forgets the
session it remembered. The overlay keeps whether it is home with its
reported target, so the menu needs no file system check on redraw.
2026-10-05 00:21:53 +02:00
devmobasa f4109dd5ba feat(daemon): continue the overlay's last session after hide and show
The daemon now remembers the session its overlay last reported and starts
the next overlay there. The remembered session is two-valued, home or a
named file, lives only in daemon memory, and is resolved when the child is
spawned, so a report read after a request was queued still applies.

The child owner records the start identity proved at readiness and reads
the child's final report inside retirement, before it releases that
identity, on natural exit, on stop and on forced reap. A report must be a
private regular file carrying exactly that generation, PID and start time;
anything else is logged and ignored, and the report is removed either way.
Startup removes reports an earlier daemon left without restoring anything
from them.

Launches carry optional variables: a capability marker, the daemon's home
session file, and the remembered session as the one to continue. The
command line is unchanged, so an older overlay still starts at home. A
--daemon-toggle --session-file request outranks the remembered session for
its activation; a request for home itself starts at home and forgets the
remembered session. The visible-target guard uses the session the running
overlay last reported. The broker allows the new variables and strips them
from helpers that are not wayscriber relaunches.
2026-10-05 00:10:52 +02:00
devmobasa ac35f7c31d feat(session): continue a remembered session and report session changes
A daemon overlay can now be told, through optional launch variables, the
daemon's home session and a remembered session to continue in place of
home. An explicit --session-file other than home outranks the remembered
one. Before the first load, the overlay checks that the remembered session
still exists; when it was moved or deleted it starts at home instead and
says so, rather than creating an empty session at the missing path.

Whenever an explicit command commits a new target, and when the first load
falls back home, the overlay reports its session to the daemon in a
per-generation record under daemon-commands/overlay-targets/, outside the
strict v2 tree. Home is reported as home even when it is a named file, an
unchanged target is not reported again, and only a daemon that advertises
support receives reports. A failed report leaves the switch in place and
says that the session may not reopen after the overlay hides.

No daemon sets these variables yet, so behavior is unchanged until the
daemon starts reading the reports.
2026-10-05 00:10:52 +02:00
devmobasa 1dbe5f7ef4 docs(guards): state what the no-Python guard reads and skips
The guard's header claimed it read MSBuild and packaging files in general,
but it reads only listed extensions and extensionless files; other types,
such as .csproj or .json, are checked by name. It also honors a `!` line in
.gitignore only when it names one file. Say both, and list them among the
known limits.
2026-10-04 23:33:07 +02:00
devmobasa 7c85d8d9bf test(gtk): close proxy connections before joining its listeners
A forwarder blocked on a hung peer holds its connection's state lock, so a
control command waiting for that lock kept its listener from finishing and
the proxy's teardown from returning. Close the connections first, which
wakes the forwarder, then join the listeners and close any connection
accepted meanwhile.
2026-10-04 23:32:24 +02:00
devmobasa 470a65b9af test: play external helpers with the test binary, not scripts
Four tests wrote and ran /bin/sh scripts as stand-ins for the configurator,
an initial-detach probe, and wl-copy. A test-only constructor now lets the
test binary play those helpers instead: a test links the helper's name to
the test binary and names that link in the environment, and only a start
under exactly that path plays the role, so other children run the tests.

- Configurator override and tray settings launch: exit, or record the
  arguments the tray passed.
- Initial detach: report whether the child leads its process group.
- wl-copy publication: count the bytes it was given.
- OCR path probes only need an executable file, so write an empty one.
2026-10-04 23:31:01 +02:00
devmobasa 2c5061dd20 Merge pull request #407 from devmobasa/fix/startup-activation-token
fix(wayland): recover the startup activation token GTK unsets
2026-10-04 23:17:13 +02:00
devmobasa 3a64cbdf70 fix(wayland): recover the startup activation token GTK unsets
GTK 4.16 and later save XDG_ACTIVATION_TOKEN and DESKTOP_STARTUP_ID for
their own windows and unset both as soon as libgtk-4 loads, before main.
With the default toolbar-gtk feature, the overlay's std::env read of the
token always came back empty, so the xdg fallback window (GNOME and other
compositors without layer-shell) never applied the token a launcher or the
portal global shortcut passed, and could open without focus.

- Read startup-notification variables from the launch environment,
  /proc/self/environ, falling back to the live environment without /proc.
- Use XDG_ACTIVATION_TOKEN, else DESKTOP_STARTUP_ID, keeping the trimming
  and empty-value handling.
- Forward both variables when `--active` relaunches itself detached: the
  broker relaunches with the live environment, which no longer has them.
- Reuse the reader in the fake overlay fixture.
2026-10-04 22:02:43 +02:00
devmobasa b34e6ced75 Merge pull request #406 from devmobasa/refactor/daemon-launch-lifecycle
Make daemon overlay launch and lifecycle ownership explicit
2026-10-04 19:10:37 +02:00