pub fn spawn_client_process( server_addr: &str, state: &LauncherState, input_toggle_key: &str, input_control_path: &Path, input_state_path: &Path, input_toggle_control_path: &Path, ) -> Result { let exe = std::env::current_exe()?; let mut command = Command::new(exe); command.arg("--no-launcher"); command.stdout(Stdio::piped()); command.stderr(Stdio::piped()); command.env("LESAVKA_LAUNCHER_CHILD", "1"); command.env( "LESAVKA_LAUNCHER_PARENT_PID", std::process::id().to_string(), ); if let Some(start_ticks) = super::launcher_parent_start_ticks() { command.env("LESAVKA_LAUNCHER_PARENT_START_TICKS", start_ticks); } command.env("LESAVKA_SERVER_ADDR", server_addr); command.env("LESAVKA_INPUT_TOGGLE_KEY", input_toggle_key); command.env("LESAVKA_LAUNCHER_WINDOW_TITLE", "Lesavka"); command.env("LESAVKA_FOCUS_LAUNCHER_ON_LOCAL", "1"); command.env(LAUNCHER_FOCUS_SIGNAL_ENV, launcher_focus_signal_path()); command.env( LAUNCHER_CLIPBOARD_CONTROL_ENV, launcher_clipboard_control_path(), ); command.env(INPUT_CONTROL_ENV, input_control_path); command.env(INPUT_STATE_ENV, input_state_path); command.env(TOGGLE_KEY_CONTROL_ENV, input_toggle_control_path); command.env("LESAVKA_DISABLE_VIDEO_RENDER", "1"); command.env("LESAVKA_CLIPBOARD_PASTE", "1"); let audio_gain_path = audio_gain_control_path(); let _ = write_audio_gain_request(&audio_gain_path, state.audio_gain_percent); command.env(AUDIO_GAIN_CONTROL_ENV, audio_gain_path); let mic_gain_path = mic_gain_control_path(); let _ = write_mic_gain_request(&mic_gain_path, state.mic_gain_percent); command.env(MIC_GAIN_CONTROL_ENV, mic_gain_path); let camera_preview_path = uplink_camera_preview_path(); let _ = std::fs::remove_file(&camera_preview_path); command.env(UPLINK_CAMERA_PREVIEW_ENV, camera_preview_path); let mic_level_path = uplink_mic_level_path(); let _ = std::fs::remove_file(&mic_level_path); command.env(UPLINK_MIC_LEVEL_ENV, mic_level_path); let uplink_telemetry_path = uplink_telemetry_path(); let _ = std::fs::remove_file(&uplink_telemetry_path); command.env(UPLINK_TELEMETRY_ENV, uplink_telemetry_path); for (key, value) in runtime_env_vars(state) { command.env(key, value); } Ok(command.spawn()?) } pub fn attach_child_log_streams(child: &mut RelayChild, tx: Sender) { if let Some(stdout) = child.stdout.take() { spawn_log_reader(stdout, "[relay] ", tx.clone()); } if let Some(stderr) = child.stderr.take() { spawn_log_reader(stderr, "[relay:stderr] ", tx); } } fn spawn_log_reader(reader: R, prefix: &'static str, tx: Sender) where R: std::io::Read + Send + 'static, { std::thread::spawn(move || { for line in BufReader::new(reader) .lines() .map_while(std::result::Result::ok) { let trimmed = line.trim(); if !trimmed.is_empty() { let _ = tx.send(format!("{prefix}{trimmed}")); } } }); } pub fn append_session_log(buffer: >k::TextBuffer, message: &str) { let cleaned = strip_ansi_sequences(message); let trimmed = cleaned.trim(); if trimmed.is_empty() { return; } append_clean_session_log(buffer, trimmed); } /// Appends a session log line only when it passes the selected severity filter. pub fn append_session_log_for_level( buffer: >k::TextBuffer, message: &str, level: ConsoleLogLevel, ) -> bool { let cleaned = strip_ansi_sequences(message); let trimmed = cleaned.trim(); if trimmed.is_empty() || !should_show_clean_session_log_line(trimmed, level) { return false; } append_clean_session_log(buffer, trimmed); true } #[cfg(test)] fn should_show_session_log_line(message: &str, level: ConsoleLogLevel) -> bool { let cleaned = strip_ansi_sequences(message); let trimmed = cleaned.trim(); !trimmed.is_empty() && should_show_clean_session_log_line(trimmed, level) } /// Writes a cleaned line with the GTK text tags that match its source/severity. fn append_clean_session_log(buffer: >k::TextBuffer, trimmed: &str) { let mut end = buffer.end_iter(); let tags = classify_log_tags(trimmed); if tags.is_empty() { buffer.insert(&mut end, &format!("{trimmed}\n")); } else { buffer.insert_with_tags_by_name(&mut end, &format!("{trimmed}\n"), &tags); } } pub fn copy_session_log(buffer: >k::TextBuffer) -> Result<()> { let text = buffer .text(&buffer.start_iter(), &buffer.end_iter(), false) .to_string(); copy_plain_text(&text) } pub fn copy_plain_text(text: &str) -> Result<()> { let display = gtk::gdk::Display::default() .ok_or_else(|| anyhow::anyhow!("no desktop clipboard is available in this session"))?; display.clipboard().set_text(text); Ok(()) } pub fn refresh_diagnostics_report( widgets: &LauncherWidgets, state: &LauncherState, child_running: bool, ) { let mut snapshot = SnapshotReport::from_state( state, &widgets.diagnostics_log.borrow(), quality_probe_command().to_string(), ); if child_running && !snapshot.remote_active { snapshot.recommendations.insert( 0, "The relay child is still alive while launcher state says inactive; give it a moment or reconnect before trusting throughput feel.".to_string(), ); } let rendered = snapshot.to_pretty_text(); if *widgets.diagnostics_rendered_text.borrow() == rendered { return; } let diagnostics_adjustment = widgets.diagnostics_scroll.vadjustment(); let previous_value = diagnostics_adjustment.value(); let previous_max = (diagnostics_adjustment.upper() - diagnostics_adjustment.page_size()).max(0.0); let was_at_bottom = previous_max <= 0.0 || previous_value >= (previous_max - 4.0); let popout_adjustment = widgets .diagnostics_popout_scroll .borrow() .as_ref() .map(|scroll| scroll.vadjustment()); let popout_state = popout_adjustment.as_ref().map(|adjustment| { let previous_value = adjustment.value(); let previous_max = (adjustment.upper() - adjustment.page_size()).max(0.0); let was_at_bottom = previous_max <= 0.0 || previous_value >= (previous_max - 4.0); (adjustment.clone(), previous_value, was_at_bottom) }); let restore_adjustment = |adjustment: >k::Adjustment, previous_value: f64, was_at_bottom: bool| { let max = (adjustment.upper() - adjustment.page_size()).max(0.0); let target = if was_at_bottom { max } else { previous_value.min(max) }; if (adjustment.value() - target).abs() > 1.0 { adjustment.set_value(target); } }; *widgets.diagnostics_rendered_text.borrow_mut() = rendered.clone(); let update_docked = was_at_bottom || widgets.diagnostics_label.text().is_empty(); if update_docked { widgets.diagnostics_label.set_text(&rendered); restore_adjustment(&diagnostics_adjustment, previous_value, was_at_bottom); } let update_popout = popout_state .as_ref() .map(|(_, _, was_at_bottom)| *was_at_bottom) .unwrap_or(false); if let Some(label) = widgets.diagnostics_popout_label.borrow().as_ref() && (update_popout || label.text().is_empty()) { label.set_text(&rendered); } if update_popout && let Some((adjustment, previous_value, was_at_bottom)) = popout_state.as_ref() { restore_adjustment(adjustment, *previous_value, *was_at_bottom); } glib::idle_add_local_once(move || { if update_docked { restore_adjustment(&diagnostics_adjustment, previous_value, was_at_bottom); } if update_popout && let Some((adjustment, previous_value, was_at_bottom)) = popout_state { restore_adjustment(&adjustment, previous_value, was_at_bottom); } }); }