Answer "am I actually P2P right now?" per peer. A 1 Hz session task
snapshots the selected QUIC path of every live audio connection
(IrohTransport::connection_stats), core::connstats::derive turns
consecutive snapshots into RTT/loss/bitrate (path switches and counter
resets invalidate the rate window), and the peer card shows a
Direct/Relay badge with a hover tooltip for address, loss, and up/down
bitrate. No new dependencies, no wire change.
Loopback-integration-tested against real iroh endpoints; not yet
field-verified on a 2-machine call (FEATURES.md row marked 🧪).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The UI was hardcoded to one Catppuccin Mocha palette (color literals in
view() + theme() pinned to Dark). It now sources colours from a chosen
theme's palette, selectable in Settings.
- theme.rs (NEW): Palette (13 semantic colour roles) + AppTheme enum
(Catppuccin Mocha/Macchiato/Frappe/Latte, Dracula, Nord, Tokyo Night,
Gruvbox Dark, Solarized Light, Gruvbox Light). Pure palette()/label()/
ALL/base_theme()/is_dark() + WCAG relative_luminance()/contrast_ratio().
- config: theme: AppTheme field (serde-default Mocha) + backward-compat.
- app: theme() returns config.theme.base_theme() (iced widget chrome);
view() + with_layout_picker() source colours from the palette; new
ThemeSwatch canvas widget; a Theme section of clickable swatches in
Settings; SelectTheme applies live + persists. Canvas widgets already
take colours as data, so they re-theme for free.
Tests written alongside (+7 theme, +2 config; 135 -> 144 lib): every
palette clears WCAG AA text-on-base contrast (4.5:1) with subtext/accent
>= 3:1, is_dark matches luminance direction, variants distinct/labeled,
serde round-trips, default = Mocha.
Verified live: the Settings swatch grid renders all 10 palettes and the
whole UI re-themes (screenshot-checked Latte light + Dracula dark).
clippy --all-targets clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
X11 sessions now persist and restore the window position (window_x/y) in
addition to size. Gated to X11: Wayland's xdg-shell gives clients no way
to self-position, so we center there (and iced never emits Moved on
Wayland, so window_x/y stay None). No drift across save/restore — iced's
Moved event and Position::Specific both use the window's outer position.
Also confirmed peerspeak already runs on X11 out of the box (winit
compiles both backends and auto-selects via WAYLAND_DISPLAY/DISPLAY) and
documented X11/Wayland support in FEATURES.md.
- config: window_x/window_y: Option<i32> (serde-default None).
- app: is_wayland() + pure initial_window_position() helper; a Moved
handler records position; the close path persists it.
- tests: +3 initial_window_position (X11 restore / Wayland centers /
partial-or-missing centers), +2 config (round-trip incl. negative
coords; backward-compat load without the new fields). 130 -> 135 lib.
Verified live on X11/XWayland: saved an off-center position, the window
reopened there (not centered). clippy clean incl. --all-targets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface pixelpass screen-sharing from inside a peerspeak room. peerspeak owns
voice, pixelpass owns pixels — they're never Cargo deps of each other; the
contract is pixelpass's CLI flags + its `--output json` stdout stream.
Modelled on Discord: multiple simultaneous sharers, a 🔴 Live badge + 👁 Watch
on each sharing peer's card, and in-progress shares visible to late joiners.
- New `src/screenshare` module: pure `parse_pixelpass_event` seam + `pixelpass_path`
discovery (13 unit tests), async `spawn_host` (→ ticket) and `spawn_viewer`
(→ parse connected{url} → open mpv, vlc fallback). No new deps.
- Sharing rides presence: `PeerState.sharing: Option<ticket>` (serde-defaulted),
so the existing gossip re-announce delivers the offer to late joiners for free
and a PeerUpdated fires on start/stop — no separate gossip message needed.
- core: Start/Stop/ViewShare commands; host + viewer children tracked in the
session, killed on stop/leave (kill_on_drop backstop). Viewer limit left to
pixelpass's bandwidth-measured cap.
- UI: Share/Stop button (graceful "needs pixelpass" disabled state), Live badge
+ Watch on peer cards, Sharing badge on the self card. Verified by screenshot.
- config: optional `pixelpass_path` override (hand-editable).
Tests-green; the 2-machine gossip/remote path is not yet field-verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capability list of what PeerSpeak already does, companion to
ARCHITECTURE.md, so the feature surface doesn't have to be re-derived
from the code each session. Marks each row verified / tests-green /
plumbing, and collects the outstanding 2-machine field-test debt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>