impl LauncherState { pub fn new() -> Self { Self::default() } pub fn set_routing(&mut self, routing: InputRouting) { self.routing = routing; } pub fn set_server_available(&mut self, available: bool) { self.server_available = available; } pub fn set_server_version(&mut self, version: Option) { self.server_version = version.and_then(|value| { let trimmed = value.trim(); if trimmed.is_empty() { None } else { Some(trimmed.to_string()) } }); } pub fn set_view_mode(&mut self, view_mode: ViewMode) { self.view_mode = view_mode; self.displays = match view_mode { ViewMode::Unified => [DisplaySurface::Preview, DisplaySurface::Preview], ViewMode::Breakout => [DisplaySurface::Window, DisplaySurface::Window], }; } pub fn display_surface(&self, monitor_id: usize) -> DisplaySurface { self.displays .get(monitor_id) .copied() .unwrap_or(DisplaySurface::Preview) } pub fn feed_source_preset(&self, monitor_id: usize) -> FeedSourcePreset { self.feed_sources .get(monitor_id) .copied() .unwrap_or(FeedSourcePreset::ThisEye) } pub fn set_feed_source_preset(&mut self, monitor_id: usize, preset: FeedSourcePreset) { if let Some(slot) = self.feed_sources.get_mut(monitor_id) { *slot = preset; } } pub fn feed_source_options(&self, monitor_id: usize) -> Vec { vec![ FeedSourceChoice { preset: FeedSourcePreset::ThisEye, label: FeedSourcePreset::ThisEye.label(monitor_id), }, FeedSourceChoice { preset: FeedSourcePreset::OtherEye, label: FeedSourcePreset::OtherEye.label(monitor_id), }, FeedSourceChoice { preset: FeedSourcePreset::Off, label: FeedSourcePreset::Off.label(monitor_id), }, ] } pub fn resolved_feed_monitor_id(&self, monitor_id: usize) -> Option { match self.feed_source_preset(monitor_id) { FeedSourcePreset::ThisEye => Some(monitor_id.min(1)), FeedSourcePreset::OtherEye => Some(1_usize.saturating_sub(monitor_id.min(1))), FeedSourcePreset::Off => None, } } pub fn set_display_surface(&mut self, monitor_id: usize, surface: DisplaySurface) { if let Some(slot) = self.displays.get_mut(monitor_id) { *slot = surface; self.view_mode = if self .displays .iter() .any(|display| matches!(display, DisplaySurface::Window)) { ViewMode::Breakout } else { ViewMode::Unified }; } } pub fn breakout_count(&self) -> usize { self.displays .iter() .filter(|surface| matches!(surface, DisplaySurface::Window)) .count() } pub fn preview_source_size(&self) -> PreviewSourceSize { self.preview_source } pub fn set_preview_source_profile(&mut self, width: u32, height: u32, fps: u32) { if width == 0 || height == 0 { return; } self.preview_source = PreviewSourceSize { width, height, fps: fps.max(1), }; } pub fn breakout_limit_size(&self) -> PreviewSourceSize { self.breakout_limit } pub fn set_breakout_limit_size(&mut self, width: u32, height: u32) { if width == 0 || height == 0 { return; } self.breakout_limit = PreviewSourceSize { width, height, fps: self.breakout_limit.fps.max(1), }; } pub fn breakout_display_size(&self) -> PreviewSourceSize { self.breakout_display } pub fn set_breakout_display_size(&mut self, width: u32, height: u32) { if width == 0 || height == 0 { return; } self.breakout_display = PreviewSourceSize { width, height, fps: self.breakout_display.fps.max(1), }; } pub fn capture_size_preset(&self, monitor_id: usize) -> CaptureSizePreset { normalize_capture_size_preset( self.capture_sizes .get(monitor_id) .copied() .unwrap_or(CaptureSizePreset::P1080), ) } pub fn display_capture_size_preset(&self, monitor_id: usize) -> Option { self.resolved_feed_monitor_id(monitor_id) .map(|source_id| self.capture_size_preset(source_id)) } pub fn set_capture_size_preset(&mut self, monitor_id: usize, preset: CaptureSizePreset) { let preset = normalize_capture_size_preset(preset); if let Some(slot) = self.capture_sizes.get_mut(monitor_id) { *slot = preset; } let defaults = default_profile_for_preset(self.preview_source, preset); self.set_capture_fps(monitor_id, defaults.fps); self.set_capture_bitrate_kbit(monitor_id, defaults.max_bitrate_kbit); } pub fn capture_fps(&self, monitor_id: usize) -> u32 { self.capture_fps .get(monitor_id) .copied() .unwrap_or(default_eye_source_mode().fps) .max(1) } pub fn display_capture_fps(&self, monitor_id: usize) -> Option { self.resolved_feed_monitor_id(monitor_id) .map(|source_id| self.capture_fps(source_id)) } pub fn set_capture_fps(&mut self, monitor_id: usize, fps: u32) { if let Some(slot) = self.capture_fps.get_mut(monitor_id) { *slot = fps.max(1); } } pub fn capture_bitrate_kbit(&self, monitor_id: usize) -> u32 { self.capture_bitrates_kbit .get(monitor_id) .copied() .unwrap_or(estimate_source_bitrate_kbit( default_eye_source_mode().width as i32, default_eye_source_mode().height as i32, default_eye_source_mode().fps, )) .max(800) } pub fn display_capture_bitrate_kbit(&self, monitor_id: usize) -> Option { self.resolved_feed_monitor_id(monitor_id) .map(|source_id| self.capture_bitrate_kbit(source_id)) } pub fn set_capture_bitrate_kbit(&mut self, monitor_id: usize, max_bitrate_kbit: u32) { if let Some(slot) = self.capture_bitrates_kbit.get_mut(monitor_id) { *slot = max_bitrate_kbit.max(800); } } pub fn capture_size_choice(&self, monitor_id: usize) -> CaptureSizeChoice { capture_size_choice( self.preview_source, self.capture_size_preset(monitor_id), self.capture_fps(monitor_id), self.capture_bitrate_kbit(monitor_id), ) } pub fn display_capture_size_choice(&self, monitor_id: usize) -> Option { self.resolved_feed_monitor_id(monitor_id) .map(|source_id| self.capture_size_choice(source_id)) } pub fn effective_preview_source_size(&self, monitor_id: usize) -> PreviewSourceSize { let capture = self .display_capture_size_choice(monitor_id) .unwrap_or_else(|| self.capture_size_choice(monitor_id)); PreviewSourceSize { width: capture.width.max(1) as u32, height: capture.height.max(1) as u32, fps: capture.fps.max(1), } } pub fn capture_size_options(&self) -> Vec { capture_size_options(self.preview_source) } pub fn capture_fps_options(&self) -> Vec { capture_fps_options(self.preview_source) } pub fn capture_bitrate_options(&self) -> Vec { capture_bitrate_options(self.preview_source) } pub fn breakout_size_preset(&self, monitor_id: usize) -> BreakoutSizePreset { self.breakout_sizes .get(monitor_id) .copied() .unwrap_or(BreakoutSizePreset::Source) } pub fn set_breakout_size_preset(&mut self, monitor_id: usize, preset: BreakoutSizePreset) { if let Some(slot) = self.breakout_sizes.get_mut(monitor_id) { *slot = preset; } } pub fn breakout_size_choice(&self, monitor_id: usize) -> BreakoutSizeChoice { breakout_size_choice( self.breakout_limit, self.breakout_display, self.effective_preview_source_size(monitor_id), self.breakout_size_preset(monitor_id), ) } pub fn breakout_size_options(&self, monitor_id: usize) -> Vec { breakout_size_options( self.breakout_limit, self.breakout_display, self.effective_preview_source_size(monitor_id), ) } pub fn select_camera(&mut self, camera: Option) { self.devices.camera = normalize_selection(camera); } pub fn select_camera_quality(&mut self, mode: Option) { self.camera_quality = mode; } pub fn camera_quality_options(&self, catalog: &DeviceCatalog) -> Vec { self.devices .camera .as_ref() .and_then(|camera| catalog.camera_modes.get(camera)) .cloned() .unwrap_or_default() } pub fn selected_camera_quality(&self, catalog: &DeviceCatalog) -> Option { let options = self.camera_quality_options(catalog); self.camera_quality .filter(|selected| options.contains(selected)) .or_else(|| options.first().copied()) } pub fn normalize_camera_quality(&mut self, catalog: &DeviceCatalog) { self.camera_quality = self.selected_camera_quality(catalog); } pub fn select_microphone(&mut self, microphone: Option) { self.devices.microphone = normalize_selection(microphone); } pub fn select_speaker(&mut self, speaker: Option) { self.devices.speaker = normalize_selection(speaker); } pub fn set_camera_channel_enabled(&mut self, enabled: bool) { self.channels.camera = enabled; } pub fn set_microphone_channel_enabled(&mut self, enabled: bool) { self.channels.microphone = enabled; } pub fn set_audio_channel_enabled(&mut self, enabled: bool) { self.channels.audio = enabled; } pub fn set_audio_gain_percent(&mut self, percent: u32) { self.audio_gain_percent = normalize_audio_gain_percent(percent); } pub fn audio_gain_multiplier(&self) -> f64 { self.audio_gain_percent as f64 / 100.0 } pub fn audio_gain_env_value(&self) -> String { format!("{:.3}", self.audio_gain_multiplier()) } pub fn audio_gain_label(&self) -> String { format_audio_gain_percent(self.audio_gain_percent) } pub fn set_mic_gain_percent(&mut self, percent: u32) { self.mic_gain_percent = normalize_mic_gain_percent(percent); } pub fn mic_gain_multiplier(&self) -> f64 { self.mic_gain_percent as f64 / 100.0 } pub fn mic_gain_env_value(&self) -> String { format!("{:.3}", self.mic_gain_multiplier()) } pub fn mic_gain_label(&self) -> String { format_mic_gain_percent(self.mic_gain_percent) } pub fn select_keyboard(&mut self, keyboard: Option) { self.devices.keyboard = normalize_selection(keyboard); } pub fn select_mouse(&mut self, mouse: Option) { self.devices.mouse = normalize_selection(mouse); } pub fn apply_catalog_defaults(&mut self, catalog: &DeviceCatalog) { keep_or_select_first(&mut self.devices.camera, &catalog.cameras); self.normalize_camera_quality(catalog); keep_or_select_first(&mut self.devices.microphone, &catalog.microphones); keep_or_select_first(&mut self.devices.speaker, &catalog.speakers); } pub fn set_swap_key(&mut self, swap_key: impl Into) { self.swap_key = normalize_swap_key(swap_key.into()); } pub fn begin_swap_key_binding(&mut self) -> u64 { self.swap_key_binding = true; self.swap_key_binding_token = self.swap_key_binding_token.wrapping_add(1); self.swap_key_binding_token } pub fn finish_swap_key_binding(&mut self) { self.swap_key_binding = false; } pub fn cancel_swap_key_binding(&mut self, token: u64) -> bool { if self.swap_key_binding && self.swap_key_binding_token == token { self.swap_key_binding = false; true } else { false } } pub fn complete_swap_key_binding(&mut self, swap_key: impl Into) { self.set_swap_key(swap_key); self.finish_swap_key_binding(); } pub fn start_remote(&mut self) -> bool { if self.remote_active { return false; } self.remote_active = true; true } pub fn stop_remote(&mut self) -> bool { if !self.remote_active { return false; } self.remote_active = false; true } pub fn push_note(&mut self, note: impl Into) { self.notes.push(note.into()); } pub fn set_capture_power(&mut self, power: CapturePowerStatus) { self.capture_power = power; } pub fn status_line(&self) -> String { format!( "server={} mode={} view={} active={} power={} source={}x{} d1={} d2={} s1={} s2={} camera={} camera_quality={} mic={} speaker={} channels=cam:{}/mic:{}/audio:{} audio_gain={} mic_gain={} kbd={} mouse={} swap={}", self.server_available, match self.routing { InputRouting::Local => "local", InputRouting::Remote => "remote", }, match self.view_mode { ViewMode::Unified => "unified", ViewMode::Breakout => "breakout", }, self.remote_active, if self.capture_power.enabled { "on" } else { "off" }, self.preview_source.width, self.preview_source.height, self.displays[0].label(), self.displays[1].label(), self.feed_source_preset(0).as_id(), self.feed_source_preset(1).as_id(), media_status_label(self.channels.camera, self.devices.camera.as_deref()), self.camera_quality .map(CameraMode::short_label) .unwrap_or_else(|| "default".to_string()), media_status_label(self.channels.microphone, self.devices.microphone.as_deref()), media_status_label(self.channels.audio, self.devices.speaker.as_deref()), self.channels.camera, self.channels.microphone, self.channels.audio, self.audio_gain_label(), self.mic_gain_label(), self.devices.keyboard.as_deref().unwrap_or("all"), self.devices.mouse.as_deref().unwrap_or("all"), self.swap_key, ) } }