chore: senior-review cleanup pass
- Remove Gemini's committed update_*.py regex-surgery scripts - Drop unused iroh-tickets dependency (hand-rolled ticket is used instead) - Replace ToString antipattern with Display impl on PeerSpeakTicket - Route debug log to XDG state/cache dir instead of hardcoded /home path - Clear all compiler + clippy warnings (unused imports, collapsible ifs, redundant pattern matching, missing Default) Builds clean with zero warnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
-64
@@ -324,15 +324,6 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "atomic-polyfill"
|
|
||||||
version = "1.0.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
|
|
||||||
dependencies = [
|
|
||||||
"critical-section",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -529,12 +520,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "byteorder"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -1601,7 +1586,7 @@ dependencies = [
|
|||||||
"diatomic-waker",
|
"diatomic-waker",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"spin 0.10.0",
|
"spin",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1950,15 +1935,6 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hash32"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
|
|
||||||
dependencies = [
|
|
||||||
"byteorder",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
@@ -1988,20 +1964,6 @@ dependencies = [
|
|||||||
"foldhash 0.2.0",
|
"foldhash 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "heapless"
|
|
||||||
version = "0.7.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
|
|
||||||
dependencies = [
|
|
||||||
"atomic-polyfill",
|
|
||||||
"hash32",
|
|
||||||
"rustc_version",
|
|
||||||
"serde",
|
|
||||||
"spin 0.9.8",
|
|
||||||
"stable_deref_trait",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -2812,20 +2774,6 @@ dependencies = [
|
|||||||
"ws_stream_wasm",
|
"ws_stream_wasm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "iroh-tickets"
|
|
||||||
version = "1.0.0-rc.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0a4b7fbfa10582f6b4f6b013eef1d21987d3df5fd42c0f7707d5de6abd34f8e9"
|
|
||||||
dependencies = [
|
|
||||||
"data-encoding",
|
|
||||||
"derive_more",
|
|
||||||
"iroh-base",
|
|
||||||
"n0-error",
|
|
||||||
"postcard",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irpc"
|
name = "irpc"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
@@ -4180,7 +4128,6 @@ dependencies = [
|
|||||||
"iced",
|
"iced",
|
||||||
"iroh",
|
"iroh",
|
||||||
"iroh-gossip",
|
"iroh-gossip",
|
||||||
"iroh-tickets",
|
|
||||||
"opus",
|
"opus",
|
||||||
"pipewire",
|
"pipewire",
|
||||||
"rand",
|
"rand",
|
||||||
@@ -4405,7 +4352,6 @@ dependencies = [
|
|||||||
"cobs",
|
"cobs",
|
||||||
"embedded-io 0.4.0",
|
"embedded-io 0.4.0",
|
||||||
"embedded-io 0.6.1",
|
"embedded-io 0.6.1",
|
||||||
"heapless",
|
|
||||||
"postcard-derive",
|
"postcard-derive",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -5284,15 +5230,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spin"
|
|
||||||
version = "0.9.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
|
||||||
dependencies = [
|
|
||||||
"lock_api",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ dirs = "6.0.0"
|
|||||||
iced = "0.14.0"
|
iced = "0.14.0"
|
||||||
iroh = "1.0.0-rc.0"
|
iroh = "1.0.0-rc.0"
|
||||||
iroh-gossip = "0.99.0"
|
iroh-gossip = "0.99.0"
|
||||||
iroh-tickets = "1.0.0-rc.0"
|
|
||||||
opus = "0.3.1"
|
opus = "0.3.1"
|
||||||
pipewire = "0.9"
|
pipewire = "0.9"
|
||||||
rand = "0.10.1"
|
rand = "0.10.1"
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ use crate::audio::pw_cli::{AudioDevice, enumerate_audio_devices};
|
|||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
|
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
container, column, row, text, button, text_input, scrollable, slider, checkbox, pick_list, Column, Space,
|
container, column, row, text, button, text_input, scrollable, slider, checkbox, pick_list, Column,
|
||||||
};
|
};
|
||||||
use iced::{
|
use iced::{
|
||||||
Color, Background, Border, Element, Subscription, Task, Theme, Event, keyboard,
|
Color, Background, Border, Element, Subscription, Task, Theme, Event, keyboard,
|
||||||
@@ -143,7 +143,7 @@ pub fn run_gui() -> iced::Result {
|
|||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(state: &AppState) -> Subscription<AppMessage> {
|
fn subscription(_state: &AppState) -> Subscription<AppMessage> {
|
||||||
let core_sub = Subscription::run(core_subscription).map(AppMessage::UiEventReceived);
|
let core_sub = Subscription::run(core_subscription).map(AppMessage::UiEventReceived);
|
||||||
let event_sub = iced::event::listen().map(AppMessage::EventOccurred);
|
let event_sub = iced::event::listen().map(AppMessage::EventOccurred);
|
||||||
Subscription::batch(vec![core_sub, event_sub])
|
Subscription::batch(vec![core_sub, event_sub])
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ struct PlaybackState {
|
|||||||
thread: JoinHandle<()>,
|
thread: JoinHandle<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for PipeWireBackend {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PipeWireBackend {
|
impl PipeWireBackend {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
pw::init();
|
pw::init();
|
||||||
|
|||||||
+1
-2
@@ -1,10 +1,9 @@
|
|||||||
use peerspeak::network::{
|
use peerspeak::network::{
|
||||||
gossip::IrohGossipState,
|
gossip::IrohGossipState,
|
||||||
RoomState, PeerState, RoomEvent,
|
RoomState, PeerState,
|
||||||
};
|
};
|
||||||
use iroh::{Endpoint, endpoint::presets};
|
use iroh::{Endpoint, endpoint::presets};
|
||||||
use iroh_gossip::net::Gossip;
|
use iroh_gossip::net::Gossip;
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::time::{self, Duration};
|
use tokio::time::{self, Duration};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|||||||
+3
-5
@@ -29,13 +29,11 @@ impl AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load() -> Self {
|
pub fn load() -> Self {
|
||||||
if let Some(path) = Self::config_path() {
|
if let Some(path) = Self::config_path()
|
||||||
if let Ok(contents) = fs::read_to_string(&path) {
|
&& let Ok(contents) = fs::read_to_string(&path)
|
||||||
if let Ok(config) = serde_json::from_str(&contents) {
|
&& let Ok(config) = serde_json::from_str(&contents) {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-1
@@ -5,11 +5,30 @@ pub mod core;
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
/// Resolves the log file path once: `$XDG_STATE_HOME/peerspeak/peerspeak.log`
|
||||||
|
/// (via `dirs::state_dir`), falling back to the system temp dir. Computed lazily
|
||||||
|
/// so we never hardcode a per-user path.
|
||||||
|
fn log_path() -> &'static PathBuf {
|
||||||
|
static LOG_PATH: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
LOG_PATH.get_or_init(|| {
|
||||||
|
let mut dir = dirs::state_dir()
|
||||||
|
.or_else(dirs::cache_dir)
|
||||||
|
.unwrap_or_else(std::env::temp_dir);
|
||||||
|
dir.push("peerspeak");
|
||||||
|
let _ = std::fs::create_dir_all(&dir);
|
||||||
|
dir.push("peerspeak.log");
|
||||||
|
dir
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn log_msg(msg: &str) {
|
pub fn log_msg(msg: &str) {
|
||||||
if let Ok(mut file) = std::fs::OpenOptions::new()
|
if let Ok(mut file) = std::fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
.open("/home/mollusk/peerspeak.log")
|
.open(log_path())
|
||||||
{
|
{
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
if let Ok(time) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
|
if let Ok(time) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
|
||||||
|
|||||||
@@ -114,12 +114,11 @@ impl RoomState for IrohGossipState {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(payload) = initial_payload {
|
if let Some(payload) = initial_payload
|
||||||
if let Ok(bytes) = serde_json::to_vec(&payload) {
|
&& let Ok(bytes) = serde_json::to_vec(&payload) {
|
||||||
crate::log_msg(&format!("Broadcasting initial state from self_id={:?}", self_id));
|
crate::log_msg(&format!("Broadcasting initial state from self_id={:?}", self_id));
|
||||||
let _ = gossip_sender_clone.broadcast(bytes.into()).await;
|
let _ = gossip_sender_clone.broadcast(bytes.into()).await;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Stream topic messages
|
// Stream topic messages
|
||||||
while let Some(res) = gossip_receiver.next().await {
|
while let Some(res) = gossip_receiver.next().await {
|
||||||
@@ -186,13 +185,12 @@ impl RoomState for IrohGossipState {
|
|||||||
msg: GossipMessage::Announce(state.clone()),
|
msg: GossipMessage::Announce(state.clone()),
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
if let Some(payload) = payload_opt {
|
if let Some(payload) = payload_opt
|
||||||
if let Ok(bytes) = serde_json::to_vec(&payload) {
|
&& let Ok(bytes) = serde_json::to_vec(&payload) {
|
||||||
crate::log_msg(&format!("Broadcasting state to new neighbor={:?}", peer_id));
|
crate::log_msg(&format!("Broadcasting state to new neighbor={:?}", peer_id));
|
||||||
let _ = gossip_sender_clone.broadcast(bytes.into()).await;
|
let _ = gossip_sender_clone.broadcast(bytes.into()).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(iroh_gossip::api::Event::NeighborDown(peer_id)) => {
|
Ok(iroh_gossip::api::Event::NeighborDown(peer_id)) => {
|
||||||
crate::log_msg(&format!("Gossip event: NeighborDown={:?}", peer_id));
|
crate::log_msg(&format!("Gossip event: NeighborDown={:?}", peer_id));
|
||||||
let removed = peers.lock().unwrap().remove(&peer_id).is_some();
|
let removed = peers.lock().unwrap().remove(&peer_id).is_some();
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ impl NetworkTransport for IrohTransport {
|
|||||||
loop {
|
loop {
|
||||||
match conn_clone.read_datagram().await {
|
match conn_clone.read_datagram().await {
|
||||||
Ok(bytes) => {
|
Ok(bytes) => {
|
||||||
if let Err(_) = incoming_tx_inner.send((peer_id, bytes)).await {
|
if incoming_tx_inner.send((peer_id, bytes)).await.is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -42,11 +42,16 @@ pub struct PeerSpeakTicket {
|
|||||||
pub topic_id: [u8; 32],
|
pub topic_id: [u8; 32],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for PeerSpeakTicket {
|
impl std::fmt::Display for PeerSpeakTicket {
|
||||||
fn to_string(&self) -> String {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let serialized = serde_json::to_vec(self).unwrap();
|
// serde_json on a struct of String/[u8;32] fields is infallible in practice,
|
||||||
// Convert to base64 URL-safe string
|
// but Display can't surface an error, so fall back to an empty ticket body.
|
||||||
base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, &serialized)
|
let serialized = serde_json::to_vec(self).unwrap_or_default();
|
||||||
|
let encoded = base64::Engine::encode(
|
||||||
|
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
|
||||||
|
&serialized,
|
||||||
|
);
|
||||||
|
f.write_str(&encoded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-139
@@ -1,139 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
with open("src/app/mod.rs", "r") as f:
|
|
||||||
text = f.read()
|
|
||||||
|
|
||||||
# 1. Imports
|
|
||||||
text = text.replace(
|
|
||||||
"use crate::core::{CoreController, messages::{CoreCommand, UiEvent}};\nuse crate::network::PeerState;",
|
|
||||||
"use crate::core::{CoreController, messages::{CoreCommand, UiEvent}};\nuse crate::network::PeerState;\nuse crate::audio::pw_cli::{AudioDevice, enumerate_audio_devices};\nuse crate::config::AppConfig;"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Iced imports
|
|
||||||
text = text.replace(
|
|
||||||
"container, column, row, text, button, text_input, scrollable, slider, checkbox, Column, Space,",
|
|
||||||
"container, column, row, text, button, text_input, scrollable, slider, checkbox, pick_list, Column, Space,"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. AppMessage
|
|
||||||
text = text.replace(
|
|
||||||
"InputDeviceChanged(String),\n OutputDeviceChanged(String),",
|
|
||||||
"InputDeviceSelected(AudioDevice),\n OutputDeviceSelected(AudioDevice),"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. AppState
|
|
||||||
text = text.replace(
|
|
||||||
"input_device: String,\n output_device: String,",
|
|
||||||
"input_devices: Vec<AudioDevice>,\n output_devices: Vec<AudioDevice>,\n selected_input: Option<AudioDevice>,\n selected_output: Option<AudioDevice>,\n config: AppConfig,"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 5. AppState::default()
|
|
||||||
default_start = text.find("let _ = UI_RX.set(Mutex::new(Some(ui_rx)));")
|
|
||||||
if default_start != -1:
|
|
||||||
default_end = default_start + len("let _ = UI_RX.set(Mutex::new(Some(ui_rx)));")
|
|
||||||
insertion = """
|
|
||||||
let config = AppConfig::load();
|
|
||||||
let all_devices = enumerate_audio_devices();
|
|
||||||
let input_devices: Vec<_> = all_devices.iter().filter(|d| d.is_input).cloned().collect();
|
|
||||||
let output_devices: Vec<_> = all_devices.iter().filter(|d| !d.is_input).cloned().collect();
|
|
||||||
|
|
||||||
let selected_input = input_devices.iter().find(|d| d.name == config.input_device).cloned();
|
|
||||||
let selected_output = output_devices.iter().find(|d| d.name == config.output_device).cloned();
|
|
||||||
"""
|
|
||||||
text = text[:default_end] + "\n" + insertion + text[default_end:]
|
|
||||||
|
|
||||||
text = text.replace(
|
|
||||||
"input_device: \"\".to_string(),\n output_device: \"\".to_string(),",
|
|
||||||
"input_devices,\n output_devices,\n selected_input,\n selected_output,\n config,"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 6. JoinPressed / CreatePressed
|
|
||||||
join_old = """ AppMessage::JoinPressed => {
|
|
||||||
state.status_message = "Connecting to room...".to_string();
|
|
||||||
let _ = state.controller.send(CoreCommand::Join {
|
|
||||||
name: state.name.clone(),
|
|
||||||
ticket: state.ticket_input.clone(),
|
|
||||||
input_device: if state.input_device.is_empty() { None } else { Some(state.input_device.clone()) },
|
|
||||||
output_device: if state.output_device.is_empty() { None } else { Some(state.output_device.clone()) },
|
|
||||||
});
|
|
||||||
}"""
|
|
||||||
join_new = """ AppMessage::JoinPressed => {
|
|
||||||
let input_device = state.selected_input.as_ref().map(|d| d.name.clone());
|
|
||||||
let output_device = state.selected_output.as_ref().map(|d| d.name.clone());
|
|
||||||
if !state.ticket_input.is_empty() {
|
|
||||||
state.status_message = "Joining room...".to_string();
|
|
||||||
let _ = state.controller.send(CoreCommand::Join {
|
|
||||||
name: state.name.clone(),
|
|
||||||
ticket: state.ticket_input.clone(),
|
|
||||||
input_device,
|
|
||||||
output_device,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}"""
|
|
||||||
text = text.replace(join_old, join_new)
|
|
||||||
|
|
||||||
create_old = """ AppMessage::CreatePressed => {
|
|
||||||
state.status_message = "Creating room...".to_string();
|
|
||||||
let _ = state.controller.send(CoreCommand::Join {
|
|
||||||
name: state.name.clone(),
|
|
||||||
ticket: "".to_string(),
|
|
||||||
input_device: if state.input_device.is_empty() { None } else { Some(state.input_device.clone()) },
|
|
||||||
output_device: if state.output_device.is_empty() { None } else { Some(state.output_device.clone()) },
|
|
||||||
});
|
|
||||||
}"""
|
|
||||||
create_new = """ AppMessage::CreatePressed => {
|
|
||||||
let input_device = state.selected_input.as_ref().map(|d| d.name.clone());
|
|
||||||
let output_device = state.selected_output.as_ref().map(|d| d.name.clone());
|
|
||||||
state.status_message = "Creating room...".to_string();
|
|
||||||
let _ = state.controller.send(CoreCommand::Join {
|
|
||||||
name: state.name.clone(),
|
|
||||||
ticket: "create".to_string(),
|
|
||||||
input_device,
|
|
||||||
output_device,
|
|
||||||
});
|
|
||||||
}"""
|
|
||||||
text = text.replace(create_old, create_new)
|
|
||||||
|
|
||||||
# 7. DeviceChanged -> DeviceSelected
|
|
||||||
text = text.replace(
|
|
||||||
"AppMessage::InputDeviceChanged(val) => {\n state.input_device = val;\n }",
|
|
||||||
"AppMessage::InputDeviceSelected(dev) => {\n state.config.input_device = dev.name.clone();\n state.config.save();\n state.selected_input = Some(dev);\n }"
|
|
||||||
)
|
|
||||||
text = text.replace(
|
|
||||||
"AppMessage::OutputDeviceChanged(val) => {\n state.output_device = val;\n }",
|
|
||||||
"AppMessage::OutputDeviceSelected(dev) => {\n state.config.output_device = dev.name.clone();\n state.config.save();\n state.selected_output = Some(dev);\n }"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 8. View Settings
|
|
||||||
settings_old = """ text("Device Settings (Optional Node Target IDs)").size(14).color(color_subtext),
|
|
||||||
row![
|
|
||||||
text_input("Input Target", &state.input_device).on_input(AppMessage::InputDeviceChanged).style(t_style.clone()).padding(8),
|
|
||||||
horizontal_space(),
|
|
||||||
text_input("Output Target", &state.output_device).on_input(AppMessage::OutputDeviceChanged).style(t_style.clone()).padding(8),
|
|
||||||
].spacing(10),"""
|
|
||||||
|
|
||||||
settings_new = """ text("Device Settings").size(14).color(color_subtext),
|
|
||||||
row![
|
|
||||||
column![
|
|
||||||
text("Input Target").size(12).color(color_subtext),
|
|
||||||
pick_list(
|
|
||||||
&state.input_devices[..],
|
|
||||||
state.selected_input.clone(),
|
|
||||||
AppMessage::InputDeviceSelected,
|
|
||||||
).width(iced::Length::Fixed(160.0))
|
|
||||||
].spacing(4),
|
|
||||||
horizontal_space(),
|
|
||||||
column![
|
|
||||||
text("Output Target").size(12).color(color_subtext),
|
|
||||||
pick_list(
|
|
||||||
&state.output_devices[..],
|
|
||||||
state.selected_output.clone(),
|
|
||||||
AppMessage::OutputDeviceSelected,
|
|
||||||
).width(iced::Length::Fixed(160.0))
|
|
||||||
].spacing(4),
|
|
||||||
].spacing(10),"""
|
|
||||||
|
|
||||||
text = text.replace(settings_old, settings_new)
|
|
||||||
|
|
||||||
with open("src/app/mod.rs", "w") as f:
|
|
||||||
f.write(text)
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
with open("src/app/mod.rs", "r") as f:
|
|
||||||
text = f.read()
|
|
||||||
|
|
||||||
# 1. Add to AppMessage
|
|
||||||
text = text.replace(
|
|
||||||
" OutputDeviceSelected(AudioDevice),\n EventOccurred(Event),",
|
|
||||||
" OutputDeviceSelected(AudioDevice),\n NoiseGateChanged(f32),\n EventOccurred(Event),"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Add to AppState (already has config, so we can just read from config.noise_gate_threshold, but state needs it too if we don't want to use state.config everywhere)
|
|
||||||
# Let's just use state.config.noise_gate_threshold
|
|
||||||
|
|
||||||
# 3. Add to update() match
|
|
||||||
update_old = """ AppMessage::OutputDeviceSelected(dev) => {
|
|
||||||
state.config.output_device = dev.name.clone();
|
|
||||||
state.config.save();
|
|
||||||
state.selected_output = Some(dev);
|
|
||||||
}"""
|
|
||||||
update_new = """ AppMessage::OutputDeviceSelected(dev) => {
|
|
||||||
state.config.output_device = dev.name.clone();
|
|
||||||
state.config.save();
|
|
||||||
state.selected_output = Some(dev);
|
|
||||||
}
|
|
||||||
AppMessage::NoiseGateChanged(val) => {
|
|
||||||
state.config.noise_gate_threshold = val;
|
|
||||||
state.config.save();
|
|
||||||
let _ = state.controller.send(CoreCommand::SetNoiseGateThreshold(val));
|
|
||||||
}"""
|
|
||||||
text = text.replace(update_old, update_new)
|
|
||||||
|
|
||||||
# 4. We also need to send the initial noise gate value to the controller on Join/Create,
|
|
||||||
# but wait! CoreController receives it async. Maybe we send it right after controller is created?
|
|
||||||
# Or just in AppState::default() we can't send because it's async? No, controller.send() is async? Actually it's non-blocking channel send.
|
|
||||||
# Let's just add it to Join message or send it when RoomJoined occurs.
|
|
||||||
# Wait, let's just send it when creating the controller? No, `state.controller.send()` returns Result, it's non-blocking.
|
|
||||||
default_old = """ let controller = Arc::new(CoreController::new(ui_tx));
|
|
||||||
let _ = UI_RX.set(Mutex::new(Some(ui_rx)));
|
|
||||||
|
|
||||||
let config = AppConfig::load();"""
|
|
||||||
default_new = """ let controller = Arc::new(CoreController::new(ui_tx));
|
|
||||||
let _ = UI_RX.set(Mutex::new(Some(ui_rx)));
|
|
||||||
|
|
||||||
let config = AppConfig::load();
|
|
||||||
let _ = controller.send(CoreCommand::SetNoiseGateThreshold(config.noise_gate_threshold));"""
|
|
||||||
text = text.replace(default_old, default_new)
|
|
||||||
|
|
||||||
# 5. Add slider to view_settings
|
|
||||||
settings_old = """ ].spacing(10),
|
|
||||||
vertical_space(30.0),
|
|
||||||
button("""
|
|
||||||
|
|
||||||
settings_new = """ ].spacing(10),
|
|
||||||
vertical_space(20.0),
|
|
||||||
column![
|
|
||||||
text(&format!("Mic Sensitivity (Noise Gate): {:.1}%", state.config.noise_gate_threshold * 100.0)).size(14).color(color_subtext),
|
|
||||||
slider(0.0..=0.1, state.config.noise_gate_threshold, AppMessage::NoiseGateChanged)
|
|
||||||
].spacing(10).width(iced::Length::Fixed(320.0)),
|
|
||||||
vertical_space(30.0),
|
|
||||||
button("""
|
|
||||||
text = text.replace(settings_old, settings_new)
|
|
||||||
|
|
||||||
with open("src/app/mod.rs", "w") as f:
|
|
||||||
f.write(text)
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
with open("src/core/mod.rs", "r") as f:
|
|
||||||
text = f.read()
|
|
||||||
|
|
||||||
# 1. Add noise_gate_threshold to state
|
|
||||||
text = text.replace(
|
|
||||||
"let ptt_active = Arc::new(AtomicBool::new(false));",
|
|
||||||
"let ptt_active = Arc::new(AtomicBool::new(false));\n let noise_gate_threshold = Arc::new(std::sync::atomic::AtomicU32::new(0.01f32.to_bits()));"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Add noise_gate_threshold to capture thread closure
|
|
||||||
text = text.replace(
|
|
||||||
"let ptt_active_clone = ptt_active.clone();",
|
|
||||||
"let ptt_active_clone = ptt_active.clone();\n let noise_gate_threshold_clone = noise_gate_threshold.clone();"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. Add noise gate logic to capture loop
|
|
||||||
old_capture_loop = """ while let Ok(pcm) = capture_rx.recv() {
|
|
||||||
if is_muted_clone.load(Ordering::Relaxed) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ptt_mode_clone.load(Ordering::Relaxed) && !ptt_active_clone.load(Ordering::Relaxed) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Ok(encoded) = encoder.encode(&pcm) {"""
|
|
||||||
|
|
||||||
new_capture_loop = """ while let Ok(pcm) = capture_rx.recv() {
|
|
||||||
if is_muted_clone.load(Ordering::Relaxed) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ptt_mode_clone.load(Ordering::Relaxed) && !ptt_active_clone.load(Ordering::Relaxed) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ng_bits = noise_gate_threshold_clone.load(Ordering::Relaxed);
|
|
||||||
let ng_thresh = f32::from_bits(ng_bits);
|
|
||||||
if ng_thresh > 0.0001 {
|
|
||||||
let mut sum_sq = 0.0f32;
|
|
||||||
for &sample in &pcm {
|
|
||||||
let normalized = (sample as f32) / 32768.0;
|
|
||||||
sum_sq += normalized * normalized;
|
|
||||||
}
|
|
||||||
let rms = (sum_sq / pcm.len() as f32).sqrt();
|
|
||||||
if rms < ng_thresh {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(encoded) = encoder.encode(&pcm) {"""
|
|
||||||
text = text.replace(old_capture_loop, new_capture_loop)
|
|
||||||
|
|
||||||
# 4. Handle CoreCommand::SetNoiseGateThreshold
|
|
||||||
old_match_end = """ CoreCommand::SetPeerVolume(peer_id, vol) => {
|
|
||||||
let mut guard = peer_volumes.lock().await;
|
|
||||||
guard.insert(peer_id, vol);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
new_match_end = """ CoreCommand::SetPeerVolume(peer_id, vol) => {
|
|
||||||
let mut guard = peer_volumes.lock().await;
|
|
||||||
guard.insert(peer_id, vol);
|
|
||||||
}
|
|
||||||
|
|
||||||
CoreCommand::SetNoiseGateThreshold(threshold) => {
|
|
||||||
noise_gate_threshold.store(threshold.to_bits(), Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
text = text.replace(old_match_end, new_match_end)
|
|
||||||
|
|
||||||
with open("src/core/mod.rs", "w") as f:
|
|
||||||
f.write(text)
|
|
||||||
Reference in New Issue
Block a user