Skip to content

Commit 8c21076

Browse files
proggeramlugclaude
andcommitted
v0.2.200: fix perry setup, add audio/camera docs
- Fix `perry setup` not saving to project perry.toml (auto-creates file if missing, for all 3 platform wizards) - Add docs for audio capture API (perry/system) and camera API (perry/ui) - Update system overview, UI overview, widgets page, and SUMMARY.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0d15b47 commit 8c21076

9 files changed

Lines changed: 358 additions & 100 deletions

File tree

CLAUDE.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
88

99
Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and Cranelift for code generation.
1010

11-
**Current Version:** 0.2.199
11+
**Current Version:** 0.2.200
1212

1313
## Workflow Requirements
1414

@@ -153,6 +153,12 @@ Projects can list npm packages to compile natively instead of routing to V8. Con
153153

154154
## Recent Changes
155155

156+
### v0.2.200
157+
- **Fix `perry setup` not saving to project perry.toml**: all 3 platform wizards (iOS, Android, macOS) silently skipped writing to `perry.toml` when the file didn't exist — now auto-creates it; also removed redundant `perry_toml_path.exists()` guards on encryption_exempt writes since the file is guaranteed to exist after creation
158+
- **Audio capture API (`perry/system`)**: `audioStart`, `audioStop`, `audioGetLevel` (dB(A)), `audioGetPeak`, `audioGetWaveformSamples`, `getDeviceModel` — all 6 platforms (macOS AVAudioEngine, iOS AVAudioSession, Android AudioRecord/JNI, Linux PulseAudio, Windows WASAPI, Web getUserMedia); A-weighted IIR filter, EMA smoothing, lock-free ring buffer
159+
- **Camera API (`perry/ui`, iOS only)**: `CameraView`, `cameraStart`/`Stop`/`Freeze`/`Unfreeze`, `cameraSampleColor(x,y)` (5x5 averaged pixel sampling from CVPixelBuffer), `cameraSetOnTap` — AVCaptureSession + AVCaptureVideoPreviewLayer with dynamic ObjC delegate
160+
- **Documentation**: new `docs/src/system/audio.md` and `docs/src/ui/camera.md`; updated system overview, UI overview, widgets page, and SUMMARY.md
161+
156162
### v0.2.199
157163
- **Fix `import * as X` namespace function calls**: `X.foo()` on namespace imports fell through to `js_native_call_method` fallback instead of using pre-declared scoped wrappers — intercept in `Call { PropertyGet { ExternFuncRef } }` path after class static method check; also handles exported closures via `js_closure_callN` fallback
158164
- **Fix ScrollView invisible inside ZStack**: ZStack's `add_child` (constraint-based) was dead code — `widgets::add_child` now detects ZStack parents via handle tracking and routes to `zstack::add_child`; also set `translatesAutoresizingMaskIntoConstraints: false` on NSScrollView for proper Auto Layout

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ opt-level = "s" # Optimize for size in stdlib
7979
opt-level = 3
8080

8181
[workspace.package]
82-
version = "0.2.199"
82+
version = "0.2.200"
8383
edition = "2021"
8484
license = "MIT"
8585
repository = "https://github.com/PerryTS/perry"

crates/perry/src/commands/setup.rs

Lines changed: 68 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -221,29 +221,25 @@ pub(crate) fn android_wizard(saved: &mut PerryConfig) -> Result<()> {
221221

222222
// Update project perry.toml with distribute = "playstore"
223223
let perry_toml_path = std::env::current_dir()?.join("perry.toml");
224-
if perry_toml_path.exists() {
225-
let gp_key = saved.android.as_ref().and_then(|a| a.google_play_key_path.as_deref());
226-
match update_perry_toml_android(&perry_toml_path, &keystore_path, &key_alias, gp_key) {
227-
Ok(()) => {
228-
println!();
229-
println!(
230-
" {} Updated perry.toml with [android] settings",
231-
style("✓").green()
232-
);
233-
}
234-
Err(e) => {
235-
println!();
236-
println!(" {} Could not update perry.toml: {e}", style("!").yellow());
237-
println!(" Add these manually to your perry.toml [android] section:");
238-
println!(" distribute = \"playstore\"");
239-
}
224+
// Create perry.toml if it doesn't exist — project-specific config belongs here
225+
if !perry_toml_path.exists() {
226+
std::fs::write(&perry_toml_path, "")?;
227+
}
228+
let gp_key = saved.android.as_ref().and_then(|a| a.google_play_key_path.as_deref());
229+
match update_perry_toml_android(&perry_toml_path, &keystore_path, &key_alias, gp_key) {
230+
Ok(()) => {
231+
println!();
232+
println!(
233+
" {} Updated perry.toml with [android] settings",
234+
style("✓").green()
235+
);
236+
}
237+
Err(e) => {
238+
println!();
239+
println!(" {} Could not update perry.toml: {e}", style("!").yellow());
240+
println!(" Add these manually to your perry.toml [android] section:");
241+
println!(" distribute = \"playstore\"");
240242
}
241-
} else {
242-
println!();
243-
println!(" Add to your perry.toml:");
244-
println!();
245-
println!(" {}", style("[android]").cyan());
246-
println!(" distribute = \"playstore\"");
247243
}
248244

249245
println!();
@@ -802,28 +798,26 @@ pub(crate) fn ios_wizard(saved: &mut PerryConfig) -> Result<()> {
802798
let p12_str = p12_path.to_string_lossy().to_string();
803799
let profile_str = profile_path.to_string_lossy().to_string();
804800

805-
if perry_toml_path.exists() {
806-
match update_perry_toml_ios(
807-
&perry_toml_path,
808-
&p12_str,
809-
&profile_str,
810-
created_signing_identity.as_deref(),
811-
) {
812-
Ok(()) => {
813-
println!(" {} Project credentials saved to {}", style("✓").green().bold(),
814-
style(perry_toml_path.display()).dim());
815-
}
816-
Err(e) => {
817-
println!(" {} Could not update perry.toml: {e}", style("!").yellow());
818-
println!(" Add these manually to your perry.toml [ios] section:");
819-
println!(" certificate = \"{}\"", p12_str);
820-
println!(" provisioning_profile = \"{}\"", profile_str);
821-
}
801+
// Create perry.toml if it doesn't exist — project-specific config belongs here
802+
if !perry_toml_path.exists() {
803+
std::fs::write(&perry_toml_path, "")?;
804+
}
805+
match update_perry_toml_ios(
806+
&perry_toml_path,
807+
&p12_str,
808+
&profile_str,
809+
created_signing_identity.as_deref(),
810+
) {
811+
Ok(()) => {
812+
println!(" {} Project credentials saved to {}", style("✓").green().bold(),
813+
style(perry_toml_path.display()).dim());
814+
}
815+
Err(e) => {
816+
println!(" {} Could not update perry.toml: {e}", style("!").yellow());
817+
println!(" Add these manually to your perry.toml [ios] section:");
818+
println!(" certificate = \"{}\"", p12_str);
819+
println!(" provisioning_profile = \"{}\"", profile_str);
822820
}
823-
} else {
824-
println!(" Add these to your perry.toml [ios] section:");
825-
println!(" certificate = \"{}\"", p12_str);
826-
println!(" provisioning_profile = \"{}\"", profile_str);
827821
}
828822
// --- Export compliance ---
829823
println!(" {} Export Compliance", style("→").cyan().bold());
@@ -832,14 +826,9 @@ pub(crate) fn ios_wizard(saved: &mut PerryConfig) -> Result<()> {
832826
.with_prompt(" Does your app ONLY use standard HTTPS? (no custom encryption)")
833827
.default(true)
834828
.interact()?;
835-
if perry_toml_path.exists() {
836-
if let Err(e) = update_perry_toml_encryption_exempt(&perry_toml_path, encryption_exempt) {
837-
println!(" {} Could not update perry.toml: {e}", style("!").yellow());
838-
println!(" Add manually to [ios]: encryption_exempt = {encryption_exempt}");
839-
}
840-
} else {
841-
println!(" Add to your perry.toml [ios] section:");
842-
println!(" encryption_exempt = {encryption_exempt}");
829+
if let Err(e) = update_perry_toml_encryption_exempt(&perry_toml_path, encryption_exempt) {
830+
println!(" {} Could not update perry.toml: {e}", style("!").yellow());
831+
println!(" Add manually to [ios]: encryption_exempt = {encryption_exempt}");
843832
}
844833
println!();
845834

@@ -1118,45 +1107,32 @@ pub(crate) fn macos_wizard(saved: &mut PerryConfig) -> Result<()> {
11181107

11191108
// --- Save project-specific credentials to perry.toml ---
11201109
let perry_toml_path = std::env::current_dir()?.join("perry.toml");
1121-
if perry_toml_path.exists() {
1122-
match update_perry_toml_macos(
1123-
&perry_toml_path,
1124-
distribute_value,
1125-
&cert_path,
1126-
if signing_identity.is_empty() { None } else { Some(&signing_identity) },
1127-
if distribute_value == "both" { Some(&notarize_cert_path) } else { None },
1128-
if distribute_value == "both" && !notarize_signing_identity.is_empty() {
1129-
Some(&notarize_signing_identity)
1130-
} else {
1131-
None
1132-
},
1133-
installer_cert_path.as_deref(),
1134-
) {
1135-
Ok(()) => {
1136-
println!(" {} macOS credentials saved to {}", style("✓").green().bold(),
1137-
style(perry_toml_path.display()).dim());
1138-
}
1139-
Err(e) => {
1140-
println!(" {} Could not update perry.toml: {e}", style("!").yellow());
1141-
println!(" Add these manually to your perry.toml [macos] section:");
1142-
println!(" distribute = \"{distribute_value}\"");
1143-
println!(" certificate = \"{}\"", cert_path);
1144-
}
1145-
}
1146-
} else {
1147-
println!(" Add to your perry.toml:");
1148-
println!();
1149-
println!(" {}", style("[macos]").cyan());
1150-
println!(" distribute = \"{distribute_value}\"");
1151-
println!(" certificate = \"{}\"", cert_path);
1152-
if !signing_identity.is_empty() {
1153-
println!(" signing_identity = \"{}\"", signing_identity);
1110+
// Create perry.toml if it doesn't exist — project-specific config belongs here
1111+
if !perry_toml_path.exists() {
1112+
std::fs::write(&perry_toml_path, "")?;
1113+
}
1114+
match update_perry_toml_macos(
1115+
&perry_toml_path,
1116+
distribute_value,
1117+
&cert_path,
1118+
if signing_identity.is_empty() { None } else { Some(&signing_identity) },
1119+
if distribute_value == "both" { Some(&notarize_cert_path) } else { None },
1120+
if distribute_value == "both" && !notarize_signing_identity.is_empty() {
1121+
Some(&notarize_signing_identity)
1122+
} else {
1123+
None
1124+
},
1125+
installer_cert_path.as_deref(),
1126+
) {
1127+
Ok(()) => {
1128+
println!(" {} macOS credentials saved to {}", style("✓").green().bold(),
1129+
style(perry_toml_path.display()).dim());
11541130
}
1155-
if distribute_value == "both" {
1156-
println!(" notarize_certificate = \"{}\"", notarize_cert_path);
1157-
if !notarize_signing_identity.is_empty() {
1158-
println!(" notarize_signing_identity = \"{}\"", notarize_signing_identity);
1159-
}
1131+
Err(e) => {
1132+
println!(" {} Could not update perry.toml: {e}", style("!").yellow());
1133+
println!(" Add these manually to your perry.toml [macos] section:");
1134+
println!(" distribute = \"{distribute_value}\"");
1135+
println!(" certificate = \"{}\"", cert_path);
11601136
}
11611137
}
11621138

@@ -1169,14 +1145,9 @@ pub(crate) fn macos_wizard(saved: &mut PerryConfig) -> Result<()> {
11691145
.with_prompt(" Does your app ONLY use standard HTTPS? (no custom encryption)")
11701146
.default(true)
11711147
.interact()?;
1172-
if perry_toml_path.exists() {
1173-
if let Err(e) = update_perry_toml_section_bool(&perry_toml_path, "macos", "encryption_exempt", encryption_exempt) {
1174-
println!(" {} Could not update perry.toml: {e}", style("!").yellow());
1175-
println!(" Add manually to [macos]: encryption_exempt = {encryption_exempt}");
1176-
}
1177-
} else {
1178-
println!(" Add to your perry.toml [macos] section:");
1179-
println!(" encryption_exempt = {encryption_exempt}");
1148+
if let Err(e) = update_perry_toml_section_bool(&perry_toml_path, "macos", "encryption_exempt", encryption_exempt) {
1149+
println!(" {} Could not update perry.toml: {e}", style("!").yellow());
1150+
println!(" Add manually to [macos]: encryption_exempt = {encryption_exempt}");
11801151
}
11811152
}
11821153
println!();

docs/src/SUMMARY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
- [Animation](ui/animation.md)
3333
- [Multi-Window](ui/multi-window.md)
3434
- [Theming](ui/theming.md)
35+
- [Camera](ui/camera.md)
3536

3637
# Platforms
3738

@@ -60,6 +61,7 @@
6061
- [Preferences](system/preferences.md)
6162
- [Keychain](system/keychain.md)
6263
- [Notifications](system/notifications.md)
64+
- [Audio Capture](system/audio.md)
6365
- [Other](system/other.md)
6466

6567
# Widgets

docs/src/system/audio.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Audio Capture
2+
3+
The `perry/system` module provides real-time audio capture from the device microphone, with A-weighted dB(A) level metering and waveform sampling — everything needed to build a sound meter, audio visualizer, or voice-level indicator.
4+
5+
```typescript
6+
import { audioStart, audioStop, audioGetLevel, audioGetPeak, audioGetWaveformSamples } from "perry/system";
7+
```
8+
9+
## Quick Example
10+
11+
```typescript
12+
import { App, Text, VStack, State, Canvas } from "perry/ui";
13+
import { audioStart, audioStop, audioGetLevel, audioGetPeak, audioGetWaveformSamples } from "perry/system";
14+
15+
audioStart();
16+
17+
App("Sound Meter", () => {
18+
const db = State(0);
19+
20+
// Poll the level every 100ms
21+
setInterval(() => {
22+
db.set(audioGetLevel());
23+
}, 100);
24+
25+
return VStack([
26+
Text(`${db.get()} dB`),
27+
]);
28+
});
29+
```
30+
31+
## API Reference
32+
33+
### `audioStart()`
34+
35+
Start capturing audio from the device microphone.
36+
37+
```typescript
38+
const ok = audioStart(); // 1 = success, 0 = failure
39+
```
40+
41+
On platforms that require permission (iOS, Android, Web), the system permission dialog is shown automatically. Returns `1` on success, `0` on failure (e.g., permission denied, no microphone).
42+
43+
### `audioStop()`
44+
45+
Stop audio capture and release the microphone.
46+
47+
```typescript
48+
audioStop();
49+
```
50+
51+
### `audioGetLevel()`
52+
53+
Get the current A-weighted sound level in dB(A).
54+
55+
```typescript
56+
const db = audioGetLevel(); // e.g. 45.2
57+
```
58+
59+
Returns a smoothed dB(A) value (EMA with 125ms time constant). Typical ranges:
60+
- ~30 dB — quiet room
61+
- ~50 dB — normal conversation
62+
- ~70 dB — busy street
63+
- ~90 dB — loud music
64+
- ~110+ dB — dangerously loud
65+
66+
### `audioGetPeak()`
67+
68+
Get the current peak sample amplitude.
69+
70+
```typescript
71+
const peak = audioGetPeak(); // 0.0 to 1.0
72+
```
73+
74+
Returns a normalized amplitude value (0.0 = silence, 1.0 = clipping). Useful for simple level indicators without dB conversion.
75+
76+
### `audioGetWaveformSamples(count)`
77+
78+
Get recent dB samples for waveform visualization.
79+
80+
```typescript
81+
const samples = audioGetWaveformSamples(64); // array of up to 64 dB values
82+
```
83+
84+
Returns an array of recent dB(A) readings from a 256-sample ring buffer. Pass the number of samples you want (max 256). Useful for drawing waveform displays or level history charts.
85+
86+
### `getDeviceModel()`
87+
88+
Get the device model identifier.
89+
90+
```typescript
91+
import { getDeviceModel } from "perry/system";
92+
93+
const model = getDeviceModel(); // e.g. "MacBookPro18,3", "iPhone15,2"
94+
```
95+
96+
## Platform Implementations
97+
98+
| Platform | Audio Backend | Permissions |
99+
|----------|--------------|-------------|
100+
| macOS | AVAudioEngine | Microphone permission dialog |
101+
| iOS | AVAudioSession + AVAudioEngine | System permission dialog |
102+
| Android | AudioRecord (JNI) | RECORD_AUDIO permission |
103+
| Linux | PulseAudio (libpulse-simple) | None (system-level) |
104+
| Windows | WASAPI (shared mode) | None |
105+
| Web | getUserMedia + AnalyserNode | Browser permission dialog |
106+
107+
All platforms capture at 48kHz mono and apply the same A-weighting filter (IEC 61672 standard, 3 cascaded biquad sections).
108+
109+
## Next Steps
110+
111+
- [Camera](../ui/camera.md) — Live camera preview (iOS)
112+
- [Overview](overview.md) — All system APIs

docs/src/system/overview.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
The `perry/system` module provides access to platform-native system features: preferences, secure storage, notifications, URL opening, and dark mode detection.
44

55
```typescript
6-
import { openURL, isDarkMode, preferencesSet, preferencesGet } from "perry/system";
6+
import { openURL, isDarkMode, preferencesSet, preferencesGet, audioStart, audioGetLevel } from "perry/system";
77
```
88

99
## Available APIs
@@ -19,6 +19,12 @@ import { openURL, isDarkMode, preferencesSet, preferencesGet } from "perry/syste
1919
| `sendNotification(title, body)` | Local notification | All |
2020
| `clipboardGet()` | Read clipboard | All |
2121
| `clipboardSet(text)` | Write clipboard | All |
22+
| `audioStart()` | Start microphone capture | All |
23+
| `audioStop()` | Stop microphone capture | All |
24+
| `audioGetLevel()` | Current dB(A) sound level | All |
25+
| `audioGetPeak()` | Current peak amplitude (0–1) | All |
26+
| `audioGetWaveformSamples(n)` | Recent dB samples for visualization | All |
27+
| `getDeviceModel()` | Device model identifier | All |
2228

2329
## Quick Example
2430

@@ -43,4 +49,5 @@ openURL("https://example.com");
4349
- [Preferences](preferences.md)
4450
- [Keychain](keychain.md)
4551
- [Notifications](notifications.md)
52+
- [Audio Capture](audio.md)
4653
- [Other](other.md)

0 commit comments

Comments
 (0)