217 lines
8.0 KiB
Rust

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<RelayChild> {
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<String>) {
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<R>(reader: R, prefix: &'static str, tx: Sender<String>)
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: &gtk::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: &gtk::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: &gtk::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: &gtk::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: &gtk::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);
}
});
}