Compare commits

...
Author SHA1 Message Date
mollusk 57f21a0edf Add Ayu color themes 2026-06-20 17:54:23 -04:00
molluskandClaude Opus 4.8 a30d9d5dbf Add plain-English Windows install/join guide for end users
cargo-deny / cargo-deny (push) Has been cancelled
windows-build / windows-build (push) Has been cancelled
A non-technical walkthrough to send alongside the installer: covers the
SmartScreen "unknown publisher" warning, the firewall/desktop-shortcut
checkboxes, and joining/creating a call via room tickets. Uses the actual UI
labels (Join Room / Create New Room / Copy Ticket / Leave Room).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:46:07 -04:00
molluskandClaude Opus 4.8 2a6e6401ad Add Windows installer (Inno Setup) + GUI-subsystem release builds
cargo-deny / cargo-deny (push) Has been cancelled
windows-build / windows-build (push) Has been cancelled
Package PeerSpeak for Windows as a single self-contained binary. The GUI
icon, notification chimes, and avatar presets are already embedded via
include_bytes!, and the cross-compiled .exe is statically linked (no extra
DLLs), so the installer payload is just peerspeak.exe plus an .ico.

- src/main.rs: set windows_subsystem = "windows" for release builds so the
  GUI launches without a stray console window (debug keeps the console for
  stderr/panics).
- packaging/windows/: Inno Setup script (peerspeak.iss), multi-resolution
  app icon (peerspeak.ico), and a build README. The installer drops a
  Start-menu/desktop shortcut, optionally adds a Windows Firewall allow-rule
  (iroh UDP hole-punching), and provides an uninstaller.
- win-cross-build.sh: promote the cross-build helper from a throwaway to the
  documented installer build step; .gitignore the staged exe + compiled
  setup.exe build artifacts.

Built with Inno Setup 6.7.1 under Wine; binary is unsigned (SmartScreen will
warn until code-signed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:27:03 -04:00
molluskandClaude Opus 4.8 a0a5922389 Merge reconnect-resilience: post-grace peer recovery coordinator
cargo-deny / cargo-deny (push) Has been cancelled
windows-build / windows-build (push) Has been cancelled
Fixes the field-observed dead-end where a network outage longer than the
45s anti-flap grace evicted the peer with no path back (required manual
Leave + re-ticket). On grace expiry the peer is now torn down cleanly and a
bounded per-session recovery coordinator re-bootstraps the gossip overlay
via GossipSender::join_peers on retained authenticated addresses, with
immediate-then-1/2/4/8/15/30/60s capped backoff. Readmission still requires
a fresh authenticated signed Announce, preserving the S8/S11 membership
boundary; a transport link alone cannot readmit a grace-expired peer.

Field-verified 2026-06-20 on a 2-machine Linux-host <-> Windows-VM call
through a 93s link outage on the libvirt NAT path: both UIs auto-recovered
to "2 in room" with no manual Leave/Join, exactly one Reconnected chime,
host showed "reconnecting" during the outage, and the VM log captured the
full sequence (grace expiry -> rebootstrap 1->2->4 backoff -> NeighborUp ->
authenticated Announce -> readmit -> audio link up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:01:47 -04:00
8 changed files with 307 additions and 4 deletions
+5
View File
@@ -6,3 +6,8 @@
/packaging/peerspeak/
/packaging/*.pkg.tar.*
/packaging/*.log
# Windows installer build artifacts (the staged exe + compiled setup.exe);
# the .iss script and .ico are the tracked sources.
/packaging/windows/peerspeak.exe
/packaging/windows/output/
+82
View File
@@ -0,0 +1,82 @@
# PeerSpeak — how to install and join a call (Windows)
PeerSpeak is a little voice-chat app — like a private phone call over the
internet, with no account, no signup, and no company in the middle. You install
it once, then you and I connect directly to each other.
---
## 1. Install it
1. Double-click **`peerspeak-0.2.0-setup.exe`** (the file I sent you).
2. **Windows will probably show a blue "Windows protected your PC" warning.**
This is normal — it shows up for any app that isn't from a big company with a
paid certificate. It is **not** a virus warning.
- Click **More info**
- Then click **Run anyway**
3. Windows will ask *"Do you want to allow this app to make changes?"* — click
**Yes**.
4. The setup window opens. Just keep clicking **Next**. Two checkboxes you'll
see along the way:
- **"Allow PeerSpeak through Windows Firewall"** — leave this **checked**
(it lets the call connect without interruptions).
- **"Create a desktop shortcut"** — check it if you'd like an icon on your
desktop.
5. Click **Install**, then **Finish**. PeerSpeak opens.
That's it — it's installed. You can find it again any time from the **Start
menu** (search "PeerSpeak").
---
## 2. Get on a call with me
PeerSpeak connects two people using a **room ticket** — a long code that acts
like a one-time phone number for a specific call.
**The simple way (I host):**
1. I'll create a room and send you a **ticket** (a long jumble of letters and
numbers).
2. Copy the whole ticket I sent you.
3. In PeerSpeak, paste it into the **"Join Room"** box near the bottom and press
**Join**.
4. You're in — you should see both our names listed, and we can talk.
**If you want to host instead:**
1. Type a room name and click **Create New Room**.
2. PeerSpeak gives you a **ticket** — click **Copy Ticket** and send it to me.
3. I paste it on my end and join you.
Either way works the same; it just depends on who makes the room.
---
## 3. While you're on a call
- **Your microphone** is on by default. There's a **mute** button if you need
it.
- The first time, Windows might ask for permission to use your **microphone**
click **Yes / Allow**.
- If you can't hear me or I can't hear you, open **Settings** (top right) and
check that the right **microphone** and **speakers/headphones** are selected.
- To hang up, click **Leave Room**.
---
## Troubleshooting
- **"I don't hear anything."** Open Settings and pick the correct microphone and
output device. Headphones are best — they prevent echo.
- **"It won't connect."** Make sure you pasted the *entire* ticket (they're
long and easy to cut off). If it still won't connect, we may just need a fresh
ticket — they're meant to be used right away.
- **The blue warning again.** Same as install: **More info → Run anyway**. It's
the unsigned-app warning, not malware.
Any trouble, just message me and we'll sort it out.
+72
View File
@@ -0,0 +1,72 @@
# PeerSpeak — Windows installer
This directory builds a Windows setup installer for PeerSpeak using
[Inno Setup](https://jrsoftware.org/isinfo.php).
PeerSpeak ships as a **single self-contained `peerspeak.exe`** — the GUI icon,
notification chimes, and avatar presets are all embedded in the binary
(`include_bytes!`), and the executable is statically linked against the GNU
runtime, so there are no extra DLLs to bundle. The installer payload is just the
`.exe` plus an `.ico` for the Start-menu / desktop shortcuts.
## Files
| File | Tracked | Purpose |
|------|---------|---------|
| `peerspeak.iss` | yes | Inno Setup script |
| `peerspeak.ico` | yes | multi-resolution app icon (from `assets/icons/*.png`) |
| `README.md` | yes | this file |
| `peerspeak.exe` | no (gitignored) | staged build artifact, copied from `target/x86_64-pc-windows-gnu/release/` |
| `output/peerspeak-<ver>-setup.exe` | no (gitignored) | the compiled installer |
## Build steps
1. **Cross-compile the Windows binary** (from the repo root, inside the
`peerspeak-win` archlinux distrobox):
```sh
RUSTC_BOOTSTRAP=1 ./win-cross-build.sh -Z build-std=std,panic_abort
```
This needs the `rust-src` component and the `x86_64-pc-windows-gnu` target
installed in that toolchain. The result is a statically-linked,
GUI-subsystem `.exe` (no stray console window).
2. **Stage the binary** next to the script:
```sh
cp target/x86_64-pc-windows-gnu/release/peerspeak.exe packaging/windows/
```
3. **Regenerate the icon** if the source PNGs changed:
```sh
magick assets/icons/peerspeak-16.png assets/icons/peerspeak-24.png \
assets/icons/peerspeak-32.png assets/icons/peerspeak-48.png \
assets/icons/peerspeak-64.png assets/icons/peerspeak-128.png \
assets/icons/peerspeak-256.png packaging/windows/peerspeak.ico
```
4. **Compile the installer** with Inno Setup. On Linux this runs under Wine:
```sh
cd packaging/windows
wine ~/.wine/drive_c/InnoSetup6/ISCC.exe peerspeak.iss
```
The installer lands at `output/peerspeak-<version>-setup.exe`.
## What the installer does
- Installs `peerspeak.exe` to `Program Files\PeerSpeak` (requires admin / one
UAC prompt).
- Creates a Start-menu shortcut, with an optional desktop shortcut.
- Optionally adds a Windows Firewall allow-rule for PeerSpeak (recommended —
iroh uses UDP hole-punching, so this avoids a mid-call firewall prompt). The
rule is removed on uninstall.
- Provides a standard uninstaller.
> **Note:** the installer and the binary are **not code-signed**, so Windows
> SmartScreen will show an "unknown publisher" warning on first run. The user
> clicks *More info → Run anyway*. Removing this warning requires a paid
> code-signing certificate.
Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

+63
View File
@@ -0,0 +1,63 @@
; Inno Setup script for PeerSpeak (Windows installer).
;
; PeerSpeak is a single self-contained binary: the GUI icon, notification
; chimes, and avatar presets are all embedded in the .exe (include_bytes!),
; so the only payload here is peerspeak.exe plus an .ico for the shortcuts.
;
; Build (under Wine on Linux, or native Windows):
; wine "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" peerspeak.iss
; Output lands in .\output\peerspeak-<version>-setup.exe
;
; The peerspeak.exe is cross-compiled with win-cross-build.sh
; (x86_64-pc-windows-gnu, statically linked -- no extra DLLs needed).
#define MyAppName "PeerSpeak"
#define MyAppVersion "0.2.0"
#define MyAppPublisher "mollusk"
#define MyAppExeName "peerspeak.exe"
[Setup]
; A stable AppId keeps upgrades/uninstall tracking consistent across versions.
AppId={{2754D6C1-C8A4-4B13-9824-2D303439739D}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
UninstallDisplayIcon={app}\{#MyAppExeName}
SetupIconFile=peerspeak.ico
Compression=lzma2/max
SolidCompression=yes
WizardStyle=modern
OutputDir=output
OutputBaseFilename=peerspeak-{#MyAppVersion}-setup
; Program Files install + firewall rule both need elevation.
PrivilegesRequired=admin
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "firewall"; Description: "Allow PeerSpeak through Windows Firewall (recommended for voice calls)"; GroupDescription: "Network:"
[Files]
Source: "peerspeak.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "peerspeak.ico"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\peerspeak.ico"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\peerspeak.ico"; Tasks: desktopicon
[Run]
; iroh uses UDP hole-punching; pre-authorizing avoids a mid-call firewall prompt.
Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall add rule name=""PeerSpeak"" dir=in action=allow program=""{app}\{#MyAppExeName}"" enable=yes profile=any"; Flags: runhidden; Tasks: firewall
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent
[UninstallRun]
Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall delete rule name=""PeerSpeak"""; Flags: runhidden; RunOnceId: "DelPeerSpeakFirewall"
+4
View File
@@ -1,3 +1,7 @@
// On Windows, suppress the extra console window for release GUI builds while
// keeping it in debug builds so stderr/panics stay visible during development.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
if let Err(e) = peerspeak::app::run_gui() {
eprintln!("Error running GUI: {:?}", e);
+64 -4
View File
@@ -60,6 +60,9 @@ pub enum AppTheme {
GruvboxDark,
SolarizedLight,
GruvboxLight,
AyuDark,
AyuMirage,
AyuLight,
}
/// Build a `Color` from a packed `0xRRGGBB` literal — keeps the palette tables
@@ -70,7 +73,7 @@ fn hex(c: u32) -> Color {
impl AppTheme {
/// Every theme, in picker order.
pub const ALL: [AppTheme; 10] = [
pub const ALL: [AppTheme; 13] = [
AppTheme::Mocha,
AppTheme::Macchiato,
AppTheme::Frappe,
@@ -81,6 +84,9 @@ impl AppTheme {
AppTheme::GruvboxDark,
AppTheme::SolarizedLight,
AppTheme::GruvboxLight,
AppTheme::AyuDark,
AppTheme::AyuMirage,
AppTheme::AyuLight,
];
/// Human-readable name for the picker.
@@ -96,6 +102,9 @@ impl AppTheme {
AppTheme::GruvboxDark => "Gruvbox Dark",
AppTheme::SolarizedLight => "Solarized Light",
AppTheme::GruvboxLight => "Gruvbox Light",
AppTheme::AyuDark => "Ayu Dark",
AppTheme::AyuMirage => "Ayu Mirage",
AppTheme::AyuLight => "Ayu Light",
}
}
@@ -103,7 +112,10 @@ impl AppTheme {
pub fn is_dark(self) -> bool {
!matches!(
self,
AppTheme::Latte | AppTheme::SolarizedLight | AppTheme::GruvboxLight
AppTheme::Latte
| AppTheme::SolarizedLight
| AppTheme::GruvboxLight
| AppTheme::AyuLight
)
}
@@ -120,6 +132,8 @@ impl AppTheme {
AppTheme::GruvboxDark => iced::Theme::GruvboxDark,
AppTheme::SolarizedLight => iced::Theme::SolarizedLight,
AppTheme::GruvboxLight => iced::Theme::GruvboxLight,
AppTheme::AyuDark | AppTheme::AyuMirage => iced::Theme::TokyoNight,
AppTheme::AyuLight => iced::Theme::Light,
}
}
@@ -281,6 +295,52 @@ impl AppTheme {
green: hex(0x79740e),
yellow: hex(0xb57614),
},
AppTheme::AyuDark => Palette {
crust: hex(0x06080a),
mantle: hex(0x0b0e14),
base: hex(0x0d1017),
surface: hex(0x1c222b),
overlay: hex(0x565b66),
text: hex(0xbfbdb6),
subtext: hex(0x9da1a6),
blue: hex(0xe6b450),
lavender: hex(0x59c2ff),
red: hex(0xf07178),
maroon: hex(0xff8f40),
green: hex(0xaad94c),
yellow: hex(0xffb454),
},
AppTheme::AyuMirage => Palette {
crust: hex(0x171b24),
mantle: hex(0x1a1f29),
base: hex(0x1f2430),
surface: hex(0x232834),
overlay: hex(0x707a8c),
text: hex(0xcccac2),
subtext: hex(0xa6abb4),
blue: hex(0xffcc66),
lavender: hex(0x73d0ff),
red: hex(0xf28779),
maroon: hex(0xffa759),
green: hex(0xd5ff80),
yellow: hex(0xffd173),
},
// Ayu Light's canonical orange is deepened for legibility on white.
AppTheme::AyuLight => Palette {
crust: hex(0xe6e9ec),
mantle: hex(0xf3f4f5),
base: hex(0xfcfcfc),
surface: hex(0xe8eaed),
overlay: hex(0x8a9199),
text: hex(0x5c6166),
subtext: hex(0x737980),
blue: hex(0xc7500e),
lavender: hex(0x399ee6),
red: hex(0xf07171),
maroon: hex(0xfa8d3e),
green: hex(0x86b300),
yellow: hex(0xff9940),
},
}
}
}
@@ -371,11 +431,11 @@ mod tests {
fn all_themes_distinct_and_labeled() {
// ALL covers exactly the variants once, each with a unique non-empty label
// and a distinct base colour (so swatches don't look identical).
assert_eq!(AppTheme::ALL.len(), 10);
assert_eq!(AppTheme::ALL.len(), 13);
let mut labels: Vec<&str> = AppTheme::ALL.iter().map(|t| t.label()).collect();
labels.sort_unstable();
labels.dedup();
assert_eq!(labels.len(), 10, "labels must be unique + non-empty");
assert_eq!(labels.len(), 13, "labels must be unique + non-empty");
assert!(labels.iter().all(|l| !l.is_empty()));
}
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# Cross-compile peerspeak to x86_64-pc-windows-gnu (run inside the peerspeak-win distrobox).
# Produces a statically-linked, self-contained .exe (no extra DLLs) for the
# Windows installer in packaging/windows/. Requires the rust-src component and
# the x86_64-pc-windows-gnu target; invoke with build-std for the static link:
# RUSTC_BOOTSTRAP=1 ./win-cross-build.sh -Z build-std=std,panic_abort
set -euo pipefail
cd "$(dirname "$0")"
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc
export CC_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc
export CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++
export AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar
# Bundled libopus declares cmake_minimum_required < 3.5; cmake 4.x refuses it.
export CMAKE_POLICY_VERSION_MINIMUM=3.5
cargo build --release --target x86_64-pc-windows-gnu --bin peerspeak "$@"