Skip to content

Release v0.3.1: type-driven core refactors, install fix, circular-H rebrand - #17

Merged
FAZuH merged 19 commits into
mainfrom
v0.3.1
Sep 11, 2026
Merged

FAZuH merged 19 commits into
mainfrom
v0.3.1

Conversation

@FAZuH

@FAZuH FAZuH commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Promotes the v0.3.1 release line to main.

Highlights

Contains the user's direct v0.3.1 commits (e.g. gitignore).

FAZuH added 19 commits September 3, 2026 22:48
The status reply was built by one 18-field format! in the daemon and
re-parsed by two hand-rolled readers (tray parse_status, gui
brief_status); field order was load-bearing in three places. Now
hyprlay-core::status::StatusFields owns the line in both directions:
the daemon fills the struct and sends to_wire(), the tray and GUI read
through parse_wire(), and the three liveness probes use is_status_line.
Appending a field is a one-place edit, and readers survive it by
construction (ADR-002's append-at-end convention made structural).

The wire line is byte-identical to the old format! output - proven by
the untouched tray/GUI/daemon characterization suite plus a new
full-line pin (the first the reply ever had).

Deliberate deviations from bug-for-bug, each unreachable from what the
current daemon writes (displayed().len(), channel_name().unwrap_or("-"),
ConnectionStatus::as_str):
- participants=abc parses as 0; the old tray echoed the raw token
- the tray summary shows the parsed count even when the participants
  token was empty (old: no count)
- brief_status drops the trailing separator for an empty channel value

Marker lookups require a leading space (" participants=" etc.), a
slight tightening over the old find("participants=") so a channel name
embedding a marker cannot be misread. Field types follow config.rs
(u8 scale, i32 offsets) rather than the earlier sketch (u16/i16) to
avoid lossy casts.

ADR-002's Consequences now point the convention at StatusFields;
docs/dev/code-layout.md gains the status.rs row.
refactor: build and parse the status reply in one shared type
The status reply's connection word was a bare String on both sides of
the wire contract: the daemon filled it from ConnectionStatus::to_string()
and readers took whatever token came off the line. The write side is now
sealed behind a StatusWord newtype whose only public constructor is
From<ConnectionStatus>, so no bare string can reach the wire and the word
set cannot drift from the enum's Display spelling.

The read side keeps the reader lenient bug-for-bug: parse_wire() still
fills the word from the raw token, unknown words included (a newer daemon
may already speak words this build has never seen, ADR-002 forward
compat), and Display renders the stored word unchanged - which is what
keeps to_wire() byte-identical. The tray's up/down comparison and the
GUI's brief summary read through as_str().

The wire line is byte-identical: the untouched full-line pin and the
roundtrip pin pass, and two new pins lock the two halves of the contract
(unknown word survives verbatim; the write side accepts only the sealed
vocabulary).

The type change is visible through the shared StatusFields field, so the
tray/GUI readers adapt mechanically (no behavior change) - the same
core-plus-fronts span as the C1 wire-contract commit, hence no scope.
hyprlay-core now owns a typed ServiceError for the DaemonControl /
ServiceManager port contract. Every variant transcribes its old
format! string byte-for-byte, so the observable wording on the
GUI/tray status lines and the CLI's stderr is unchanged; per-variant
Display pins in daemon_control lock that wording.

- crates/hyprlay-core/src/daemon_control.rs: ServiceError enum (20
  variants, thiserror 2), port signatures (perform, systemctl,
  spawn_daemon, quit_via_socket, install/uninstall with typed Err),
  execute_toggle renders through Display, and the Display/source
  characterization pins.
- src/platform/service/{systemd,launchd,windows}.rs: adapters
  construct ServiceError at every failure site; launchd's
  "already bootstrapped" tolerance still string-matches on the
  rendered Display.
- src/platform/service/mod.rs: install_service/uninstall_service
  return ServiceError for all three cfg-gated variants.
- tests/install.rs Spy and the GUI/tray FakeControl doubles adapt
  mechanically (failures become ServiceError constructors);
  assertions stay byte-identical.

thiserror 2.0.20 was already in Cargo.lock via iced, so the new
hyprlay-core dependency adds no new download.
refactor: type the status word and service errors
CachedUser duplicated Participant's seven persisted fields and was kept
in sync by two hand-copied From impls; roster_signature enumerated the
fields a third time, so a field added to one list silently broke write
dedup. Participant now carries the serde derives itself (speaking is
#[serde(skip)]: live-only state, filled with false on load, exactly the
old conversion semantics), Roster.users holds it directly, and
roster_signature is built from serde output so the signature and the
roster.json format can no longer disagree. Cache bytes are unchanged;
pins cover the legacy file format both ways and the per-field signature
sensitivity.
refactor(daemon): derive the roster cache from the Participant type
The three service backends repeated the same two fs sequences byte for
byte: create-parent + write (four copies under four names: systemd's
write_file/write_bytes, launchd's write_plist, windows' write_script)
and the reported remove (three identical remove_reported bodies). Both
now live once in service/fs_util, taking &[u8]; string call sites pass
.as_bytes(). Error variants, payloads, and report strings are
unchanged, so the install.rs byte and report pins pass unmodified and
windows.rs stays compile-only-verified.
refactor(platform): dedupe the service write/remove helpers into fs_util
mod.rs was a 1449-line grab-bag holding every TEA layer at once; each
concern now has exactly one home. update.rs carries the one flat update
match (the app's dispatch table, deliberately not per-domain), shortcut,
and the async toggle/auth effects. commands.rs owns Message→Command
translation plus the shared bookkeeping (mark_dirty, num_in_bounds,
revert_commands). scroll.rs owns one-page navigation: the measure
operation, section jumps, scrollspy, and the two shared widget ids.
view.rs owns the window composition (header, sidebar, status bar).
mod.rs keeps the shell: Message, Gui, run, boot, subscribe, window
settings, and the blocking send wrapper.

Behavior-identical: every moved body is byte-verbatim (verified by
mechanical extraction diff against the old file, modulo pub(super)
visibility, rustfmt line-wrapping, and forced test-path edits). All 21
tests moved with their subjects — registry/section pins to fields.rs,
command/revert pins to commands.rs, scrollspy pins to scroll.rs,
brief_status pins to view.rs, navigation state-transition pins to
update.rs — pins semantically identical. Visibility is minimal:
Message and Gui stay private in mod.rs (descendant access is free by
Rust privacy; the compiler proves it); pub(super) only on the items a
sibling or the parent actually calls. picker.rs's update import now
names the function inside the new update module.
refactor(gui): split the app core into update/commands/scroll/view
Hunting the daemon memory regression needs a series, not a btop
glance: scripts/rssprobe.sh samples VmRSS plus the VmHWM high-water
mark of a PID on an interval into a CSV, with labeled scenario
markers appended from a second shell (the mark subcommand). RSS far
below HWM means freed-but-retained memory; RSS at HWM means real
growth — the doc explains reading that pair and carries the runbook
for the hyprlayd scenario series (idle, hover-storm, VC churn).

While in docs/dev/debug-probes.md, fix two stale adapter paths
(src/adapters/ipc.rs and src/adapters/discord.rs moved under
src/daemon/adapters/).
cmd_demo exec'd target/release/tomo --config-path /tmp/tomo-demo — a
binary and flag from the project-ops template this script was copied
from. No tomo exists here, and scripts/demo.tape never did, so the
target could not run. hyprlay has no tape-driven demo to repoint it
at (the surface is a Wayland overlay, not a terminal session), so
remove the command — function, description, and help/usage mentions —
instead of shipping a dead target.
cargo install without --locked re-resolves and ignores Cargo.lock.
winit-core 0.31.0-beta.3 added NativeKeyCode::Android, which
iced_exdevtools 0.19.1 (unconditional dep of iced_layershell 0.19.1,
pinned to the 0.19 line) does not match: E0004. Pin winit-core exactly
so every resolution path lands on beta.2, and document --locked in the
README install line. Drop the pin once iced_layershell releases on
iced_exdevtools 0.20.
fix: pin winit-core for lockless installs, add the rssprobe sampler
feat: regenerate all brand assets from the circular-H icon
@github-actions

Copy link
Copy Markdown

Release Preview

Next version: v0.3.1

0.3.1 (2026-09-11)

Platforms

  • Changed the app and tray icon to a circular design
  • Fixed source installs failing on a fresh dependency resolution

@FAZuH
FAZuH merged commit e33220c into main Sep 11, 2026
7 checks passed
@FAZuH
FAZuH deleted the v0.3.1 branch September 11, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant