217 lines
8.0 KiB
Rust
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: >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);
|
|
}
|
|
});
|
|
}
|