launcher: add pause swap key and clipboard relay controls

This commit is contained in:
Brad Stein 2026-04-14 13:09:25 -03:00
parent e61a71bd61
commit 8dd3461be0
5 changed files with 203 additions and 139 deletions

View File

@ -75,8 +75,6 @@ impl InputAggregator {
} }
} }
/// Called once at startup: enumerates input devices,
/// classifies them, and constructs a aggregator struct per type.
#[cfg(coverage)] #[cfg(coverage)]
pub fn init(&mut self) -> Result<()> { pub fn init(&mut self) -> Result<()> {
let paths = std::fs::read_dir("/dev/input").context("Failed to read /dev/input")?; let paths = std::fs::read_dir("/dev/input").context("Failed to read /dev/input")?;
@ -130,7 +128,6 @@ impl InputAggregator {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
// skip anything that isn't "event*"
if !path if !path
.file_name() .file_name()
.map_or(false, |f| f.to_string_lossy().starts_with("event")) .map_or(false, |f| f.to_string_lossy().starts_with("event"))
@ -138,7 +135,6 @@ impl InputAggregator {
continue; continue;
} }
// ─── open the event node read-write *without* unsafe ──────────
let mut dev = match Device::open(&path) { let mut dev = match Device::open(&path) {
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(e) => {
@ -147,7 +143,6 @@ impl InputAggregator {
} }
}; };
// non-blocking so fetch_events never stalls the whole loop
dev.set_nonblocking(true) dev.set_nonblocking(true)
.with_context(|| format!("set_non_blocking {:?}", path))?; .with_context(|| format!("set_non_blocking {:?}", path))?;
@ -220,8 +215,6 @@ impl InputAggregator {
Ok(()) Ok(())
} }
/// We spawn the sub-aggregators in a loop or using separate tasks.
/// (For a real system: you'd spawn a separate task for each aggregator.)
#[cfg(coverage)] #[cfg(coverage)]
pub async fn run(&mut self) -> Result<()> { pub async fn run(&mut self) -> Result<()> {
loop { loop {
@ -332,6 +325,9 @@ impl InputAggregator {
m.reset_state(); m.reset_state();
} }
self.released = true; self.released = true;
if !self.pending_kill {
focus_launcher_on_local_if_enabled();
}
if self.pending_kill { if self.pending_kill {
return Ok(()); return Ok(());
} }
@ -359,7 +355,6 @@ impl InputAggregator {
tracing::info!("🧙 magic chord - freeing devices 🪄 EXPELLIARMUS!!! 🔓🕊️"); tracing::info!("🧙 magic chord - freeing devices 🪄 EXPELLIARMUS!!! 🔓🕊️");
} }
if self.released { if self.released {
// switching to remote control
for k in &mut self.keyboards { for k in &mut self.keyboards {
k.reset_state(); k.reset_state();
k.set_send(true); k.set_send(true);
@ -373,7 +368,6 @@ impl InputAggregator {
self.released = false; self.released = false;
self.pending_release = false; self.pending_release = false;
} else { } else {
// switching to local control: stop sending, keep grab until chord released
for k in &mut self.keyboards { for k in &mut self.keyboards {
k.send_empty_report(); k.send_empty_report();
k.set_send(false); k.set_send(false);
@ -396,13 +390,11 @@ impl InputAggregator {
} }
} }
/// Returns whether the configured quick-toggle key is currently pressed.
fn quick_toggle_active(&self) -> bool { fn quick_toggle_active(&self) -> bool {
self.quick_toggle_key self.quick_toggle_key
.is_some_and(|key| self.keyboards.iter().any(|kbd| kbd.has_key(key))) .is_some_and(|key| self.keyboards.iter().any(|kbd| kbd.has_key(key)))
} }
/// Applies rising-edge + debounce semantics before switching input mode.
fn observe_quick_toggle(&mut self, quick_toggle_now: bool) { fn observe_quick_toggle(&mut self, quick_toggle_now: bool) {
if quick_toggle_now && !self.quick_toggle_down { if quick_toggle_now && !self.quick_toggle_down {
let now = Instant::now(); let now = Instant::now();
@ -508,11 +500,11 @@ enum DeviceKind {
Other, Other,
} }
/// Resolves the quick-toggle key from env, defaulting to Scroll Lock. /// Resolves the quick-toggle key from env, defaulting to Pause/Break.
fn quick_toggle_key_from_env() -> Option<KeyCode> { fn quick_toggle_key_from_env() -> Option<KeyCode> {
match std::env::var("LESAVKA_INPUT_TOGGLE_KEY") { match std::env::var("LESAVKA_INPUT_TOGGLE_KEY") {
Ok(raw) => parse_quick_toggle_key(&raw), Ok(raw) => parse_quick_toggle_key(&raw),
Err(_) => Some(KeyCode::KEY_SCROLLLOCK), Err(_) => Some(KeyCode::KEY_PAUSE),
} }
} }
@ -521,11 +513,15 @@ fn parse_quick_toggle_key(raw: &str) -> Option<KeyCode> {
let normalized = raw.trim().to_ascii_lowercase(); let normalized = raw.trim().to_ascii_lowercase();
match normalized.as_str() { match normalized.as_str() {
"" | "off" | "none" | "disabled" => None, "" | "off" | "none" | "disabled" => None,
"scrolllock" | "scroll_lock" | "scroll-lock" => Some(KeyCode::KEY_SCROLLLOCK),
"sysrq" | "sysreq" | "prtsc" | "printscreen" | "print_screen" | "print-screen" => {
Some(KeyCode::KEY_SYSRQ)
}
"pause" | "pausebreak" | "pause_break" | "pause-break" => Some(KeyCode::KEY_PAUSE), "pause" | "pausebreak" | "pause_break" | "pause-break" => Some(KeyCode::KEY_PAUSE),
"f12" => Some(KeyCode::KEY_F12), "f12" => Some(KeyCode::KEY_F12),
"f11" => Some(KeyCode::KEY_F11), "f11" => Some(KeyCode::KEY_F11),
"f10" => Some(KeyCode::KEY_F10), "f10" => Some(KeyCode::KEY_F10),
_ => Some(KeyCode::KEY_SCROLLLOCK), _ => Some(KeyCode::KEY_PAUSE),
} }
} }
@ -537,3 +533,18 @@ fn quick_toggle_debounce_from_env() -> Duration {
.unwrap_or(350); .unwrap_or(350);
Duration::from_millis(millis.max(50)) Duration::from_millis(millis.max(50))
} }
#[cfg(not(coverage))]
fn focus_launcher_on_local_if_enabled() {
if std::env::var("LESAVKA_FOCUS_LAUNCHER_ON_LOCAL")
.map(|raw| raw.trim() == "0")
.unwrap_or(false)
{
return;
}
let title = std::env::var("LESAVKA_LAUNCHER_WINDOW_TITLE")
.unwrap_or_else(|_| "Lesavka Launcher".to_string());
let _ = std::process::Command::new("wmctrl")
.args(["-a", &title])
.status();
}

View File

@ -1,18 +1,19 @@
use anyhow::Result; use anyhow::{Result, anyhow};
#[cfg(not(coverage))] #[cfg(not(coverage))]
use { use {
super::devices::DeviceCatalog, super::devices::DeviceCatalog,
super::diagnostics::{ super::diagnostics::quality_probe_command,
DiagnosticsLog, PerformanceSample, SnapshotReport, quality_probe_command,
},
super::runtime_env_vars, super::runtime_env_vars,
super::state::{InputRouting, LauncherState, ViewMode}, super::state::{InputRouting, LauncherState, ViewMode},
crate::paste,
gtk::prelude::*, gtk::prelude::*,
lesavka_common::lesavka::relay_client::RelayClient,
std::cell::RefCell, std::cell::RefCell,
std::process::{Child, Command}, std::process::{Child, Command},
std::rc::Rc, std::rc::Rc,
std::time::{SystemTime, UNIX_EPOCH}, tokio::runtime::Builder as RuntimeBuilder,
tonic::{Request, transport::Channel},
}; };
#[cfg(not(coverage))] #[cfg(not(coverage))]
@ -23,7 +24,6 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let catalog = Rc::new(DeviceCatalog::discover()); let catalog = Rc::new(DeviceCatalog::discover());
let state = Rc::new(RefCell::new(LauncherState::new())); let state = Rc::new(RefCell::new(LauncherState::new()));
state.borrow_mut().apply_catalog_defaults(&catalog); state.borrow_mut().apply_catalog_defaults(&catalog);
let diagnostics = Rc::new(RefCell::new(DiagnosticsLog::new(120)));
let child_proc = Rc::new(RefCell::new(None::<Child>)); let child_proc = Rc::new(RefCell::new(None::<Child>));
let server_addr = Rc::new(server_addr); let server_addr = Rc::new(server_addr);
@ -40,7 +40,6 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
{ {
let catalog = Rc::clone(&catalog); let catalog = Rc::clone(&catalog);
let state = Rc::clone(&state); let state = Rc::clone(&state);
let diagnostics = Rc::clone(&diagnostics);
let child_proc = Rc::clone(&child_proc); let child_proc = Rc::clone(&child_proc);
let server_addr = Rc::clone(&server_addr); let server_addr = Rc::clone(&server_addr);
@ -68,10 +67,15 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
status_label.set_selectable(true); status_label.set_selectable(true);
root.append(&status_label); root.append(&status_label);
let server_label = gtk::Label::new(Some(&format!("Server: {}", server_addr.as_ref()))); let server_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
let server_label = gtk::Label::new(Some("Server"));
server_label.set_halign(gtk::Align::Start); server_label.set_halign(gtk::Align::Start);
server_label.set_selectable(true); let server_entry = gtk::Entry::new();
root.append(&server_label); server_entry.set_hexpand(true);
server_entry.set_text(server_addr.as_ref());
server_row.append(&server_label);
server_row.append(&server_entry);
root.append(&server_row);
let controls = gtk::Grid::new(); let controls = gtk::Grid::new();
controls.set_row_spacing(8); controls.set_row_spacing(8);
@ -138,19 +142,34 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
set_combo_active_text(&speaker_combo, state.borrow().devices.speaker.as_deref()); set_combo_active_text(&speaker_combo, state.borrow().devices.speaker.as_deref());
controls.attach(&speaker_combo, 1, 4, 1, 1); controls.attach(&speaker_combo, 1, 4, 1, 1);
let toggle_key_label = gtk::Label::new(Some("Input swap key"));
toggle_key_label.set_halign(gtk::Align::Start);
controls.attach(&toggle_key_label, 0, 5, 1, 1);
let toggle_key_combo = gtk::ComboBoxText::new();
toggle_key_combo.append(Some("scrolllock"), "Scroll Lock");
toggle_key_combo.append(Some("sysrq"), "SysRq / PrtSc");
toggle_key_combo.append(Some("pause"), "Pause");
toggle_key_combo.append(Some("f12"), "F12");
toggle_key_combo.append(Some("f11"), "F11");
toggle_key_combo.append(Some("f10"), "F10");
toggle_key_combo.append(Some("off"), "Disabled");
let _ = toggle_key_combo.set_active_id(Some("pause"));
controls.attach(&toggle_key_combo, 1, 5, 1, 1);
let button_row = gtk::Box::new(gtk::Orientation::Horizontal, 8); let button_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
root.append(&button_row); root.append(&button_row);
let start_button = gtk::Button::with_label("Start Session"); let start_button = gtk::Button::with_label("Start Session");
let stop_button = gtk::Button::with_label("Stop Session"); let stop_button = gtk::Button::with_label("End Relay");
let view_toggle_button = gtk::Button::with_label(""); let view_toggle_button = gtk::Button::with_label("");
let input_toggle_button = gtk::Button::with_label(""); let input_toggle_button = gtk::Button::with_label("");
let snapshot_button = gtk::Button::with_label("Save Snapshot"); let clipboard_button = gtk::Button::with_label("Send Clipboard");
button_row.append(&start_button); button_row.append(&start_button);
button_row.append(&stop_button); button_row.append(&stop_button);
button_row.append(&view_toggle_button); button_row.append(&view_toggle_button);
button_row.append(&input_toggle_button); button_row.append(&input_toggle_button);
button_row.append(&snapshot_button); button_row.append(&clipboard_button);
sync_toggle_button_labels(&state.borrow(), &view_toggle_button, &input_toggle_button); sync_toggle_button_labels(&state.borrow(), &view_toggle_button, &input_toggle_button);
let probe_hint = gtk::Label::new(Some(quality_probe_command())); let probe_hint = gtk::Label::new(Some(quality_probe_command()));
@ -159,7 +178,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
root.append(&probe_hint); root.append(&probe_hint);
let note = gtk::Label::new(Some( let note = gtk::Label::new(Some(
"Unified mode renders both streams side-by-side in one window. Use Pop Out Windows to split back into full windows. Quick input toggle key defaults to Scroll Lock.", "Unified mode renders both streams side-by-side in one window. Use Pop Out Windows to split back into full windows. Input swap key defaults to Pause and can be changed.",
)); ));
note.set_wrap(true); note.set_wrap(true);
note.set_halign(gtk::Align::Start); note.set_halign(gtk::Align::Start);
@ -167,7 +186,6 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
{ {
let state = Rc::clone(&state); let state = Rc::clone(&state);
let diagnostics = Rc::clone(&diagnostics);
let child_proc = Rc::clone(&child_proc); let child_proc = Rc::clone(&child_proc);
let status_label = status_label.clone(); let status_label = status_label.clone();
let routing_switch = routing_switch.clone(); let routing_switch = routing_switch.clone();
@ -175,6 +193,8 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let camera_combo = camera_combo.clone(); let camera_combo = camera_combo.clone();
let microphone_combo = microphone_combo.clone(); let microphone_combo = microphone_combo.clone();
let speaker_combo = speaker_combo.clone(); let speaker_combo = speaker_combo.clone();
let toggle_key_combo = toggle_key_combo.clone();
let server_entry = server_entry.clone();
let server_addr = Rc::clone(&server_addr); let server_addr = Rc::clone(&server_addr);
let view_toggle_button = view_toggle_button.clone(); let view_toggle_button = view_toggle_button.clone();
let input_toggle_button = input_toggle_button.clone(); let input_toggle_button = input_toggle_button.clone();
@ -208,23 +228,16 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
return; return;
} }
let spawn_result = { let spawn_result = relaunch_with_settings(
let mut state = state.borrow_mut(); &child_proc,
launch_or_restart_client(&child_proc, server_addr.as_ref(), &mut state) &state,
}; &server_entry,
server_addr.as_ref(),
&toggle_key_combo,
);
match spawn_result { match spawn_result {
Ok(()) => { Ok(()) => status_label.set_text(&format!("Started: {}", state.borrow().status_line())),
diagnostics.borrow_mut().record(PerformanceSample {
rtt_ms: 0.0,
input_latency_ms: 0.0,
left_fps: 0.0,
right_fps: 0.0,
dropped_frames: 0,
queue_depth: 0,
});
status_label.set_text(&format!("Started: {}", state.borrow().status_line()));
}
Err(err) => { Err(err) => {
let _ = state.borrow_mut().stop_remote(); let _ = state.borrow_mut().stop_remote();
status_label.set_text(&format!("Start failed: {err}")); status_label.set_text(&format!("Start failed: {err}"));
@ -240,18 +253,19 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
stop_button.connect_clicked(move |_| { stop_button.connect_clicked(move |_| {
stop_child_process(&child_proc); stop_child_process(&child_proc);
let _ = state.borrow_mut().stop_remote(); let _ = state.borrow_mut().stop_remote();
status_label.set_text("Stopped"); status_label.set_text("Relay ended");
}); });
} }
{ {
let state = Rc::clone(&state); let state = Rc::clone(&state);
let child_proc = Rc::clone(&child_proc); let child_proc = Rc::clone(&child_proc);
let diagnostics = Rc::clone(&diagnostics);
let status_label = status_label.clone(); let status_label = status_label.clone();
let view_combo = view_combo.clone(); let view_combo = view_combo.clone();
let input_toggle_button = input_toggle_button.clone(); let input_toggle_button = input_toggle_button.clone();
let view_toggle_button = view_toggle_button.clone(); let view_toggle_button = view_toggle_button.clone();
let toggle_key_combo = toggle_key_combo.clone();
let server_entry = server_entry.clone();
let server_addr = Rc::clone(&server_addr); let server_addr = Rc::clone(&server_addr);
let view_toggle_button_handle = view_toggle_button.clone(); let view_toggle_button_handle = view_toggle_button.clone();
view_toggle_button_handle.connect_clicked(move |_| { view_toggle_button_handle.connect_clicked(move |_| {
@ -268,25 +282,16 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
); );
if child_proc.borrow().is_some() { if child_proc.borrow().is_some() {
let spawn_result = { let spawn_result = relaunch_with_settings(
let mut state = state.borrow_mut(); &child_proc,
launch_or_restart_client(&child_proc, server_addr.as_ref(), &mut state) &state,
}; &server_entry,
server_addr.as_ref(),
&toggle_key_combo,
);
match spawn_result { match spawn_result {
Ok(()) => { Ok(()) => status_label
diagnostics.borrow_mut().record(PerformanceSample { .set_text(&format!("View switched live: {}", state.borrow().status_line())),
rtt_ms: 0.0,
input_latency_ms: 0.0,
left_fps: 0.0,
right_fps: 0.0,
dropped_frames: 0,
queue_depth: 0,
});
status_label.set_text(&format!(
"View switched live: {}",
state.borrow().status_line()
));
}
Err(err) => { Err(err) => {
let _ = state.borrow_mut().stop_remote(); let _ = state.borrow_mut().stop_remote();
status_label.set_text(&format!("View switch failed: {err}")); status_label.set_text(&format!("View switch failed: {err}"));
@ -301,11 +306,12 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
{ {
let state = Rc::clone(&state); let state = Rc::clone(&state);
let child_proc = Rc::clone(&child_proc); let child_proc = Rc::clone(&child_proc);
let diagnostics = Rc::clone(&diagnostics);
let status_label = status_label.clone(); let status_label = status_label.clone();
let routing_switch = routing_switch.clone(); let routing_switch = routing_switch.clone();
let input_toggle_button = input_toggle_button.clone(); let input_toggle_button = input_toggle_button.clone();
let view_toggle_button = view_toggle_button.clone(); let view_toggle_button = view_toggle_button.clone();
let toggle_key_combo = toggle_key_combo.clone();
let server_entry = server_entry.clone();
let server_addr = Rc::clone(&server_addr); let server_addr = Rc::clone(&server_addr);
let input_toggle_button_handle = input_toggle_button.clone(); let input_toggle_button_handle = input_toggle_button.clone();
input_toggle_button_handle.connect_clicked(move |_| { input_toggle_button_handle.connect_clicked(move |_| {
@ -322,25 +328,18 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
); );
if child_proc.borrow().is_some() { if child_proc.borrow().is_some() {
let spawn_result = { let spawn_result = relaunch_with_settings(
let mut state = state.borrow_mut(); &child_proc,
launch_or_restart_client(&child_proc, server_addr.as_ref(), &mut state) &state,
}; &server_entry,
server_addr.as_ref(),
&toggle_key_combo,
);
match spawn_result { match spawn_result {
Ok(()) => { Ok(()) => status_label.set_text(&format!(
diagnostics.borrow_mut().record(PerformanceSample { "Input mode switched live: {}",
rtt_ms: 0.0, state.borrow().status_line()
input_latency_ms: 0.0, )),
left_fps: 0.0,
right_fps: 0.0,
dropped_frames: 0,
queue_depth: 0,
});
status_label.set_text(&format!(
"Input mode switched live: {}",
state.borrow().status_line()
));
}
Err(err) => { Err(err) => {
let _ = state.borrow_mut().stop_remote(); let _ = state.borrow_mut().stop_remote();
status_label.set_text(&format!("Input switch failed: {err}")); status_label.set_text(&format!("Input switch failed: {err}"));
@ -353,32 +352,19 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
} }
{ {
let state = Rc::clone(&state); let child_proc = Rc::clone(&child_proc);
let diagnostics = Rc::clone(&diagnostics);
let status_label = status_label.clone(); let status_label = status_label.clone();
snapshot_button.connect_clicked(move |_| { let server_entry = server_entry.clone();
let report = SnapshotReport::from_state( let server_addr = Rc::clone(&server_addr);
&state.borrow(), clipboard_button.connect_clicked(move |_| {
&diagnostics.borrow(), if child_proc.borrow().is_none() {
quality_probe_command().to_string(), status_label.set_text("Start Session before sending clipboard");
); return;
let json = match report.to_pretty_json() { }
Ok(json) => json, let server_addr = selected_server_addr(&server_entry, server_addr.as_ref());
Err(err) => { match send_clipboard_to_remote(&server_addr) {
status_label.set_text(&format!("Snapshot failed: {err}")); Ok(()) => status_label.set_text("Clipboard delivered to remote"),
return; Err(err) => status_label.set_text(&format!("Clipboard send failed: {err}")),
}
};
let path = format!("/tmp/lesavka-launcher-snapshot-{}.json", now_unix_seconds());
match std::fs::write(&path, json) {
Ok(()) => {
state.borrow_mut().push_note(format!("snapshot={path}"));
status_label.set_text(&format!("Snapshot written: {path}"));
}
Err(err) => {
status_label.set_text(&format!("Snapshot write failed: {err}"));
}
} }
}); });
} }
@ -410,6 +396,78 @@ fn selected_combo_value(combo: &gtk::ComboBoxText) -> Option<String> {
}) })
} }
#[cfg(not(coverage))]
fn selected_toggle_key(combo: &gtk::ComboBoxText) -> String {
combo
.active_id()
.map(|value| value.to_string())
.unwrap_or_else(|| "pause".to_string())
}
#[cfg(not(coverage))]
fn selected_server_addr(entry: &gtk::Entry, fallback: &str) -> String {
let current = entry.text();
let trimmed = current.trim();
if trimmed.is_empty() {
fallback.to_string()
} else {
trimmed.to_string()
}
}
#[cfg(not(coverage))]
/// Applies the current server/key launcher controls and relaunches the child session.
fn relaunch_with_settings(
child_proc: &Rc<RefCell<Option<Child>>>,
state: &Rc<RefCell<LauncherState>>,
server_entry: &gtk::Entry,
server_fallback: &str,
toggle_key_combo: &gtk::ComboBoxText,
) -> Result<()> {
let server_addr = selected_server_addr(server_entry, server_fallback);
let input_toggle_key = selected_toggle_key(toggle_key_combo);
let mut state = state.borrow_mut();
launch_or_restart_client(child_proc, &server_addr, &mut state, &input_toggle_key)
}
#[cfg(not(coverage))]
/// Reads local clipboard text and sends it to the remote server's paste RPC.
fn send_clipboard_to_remote(server_addr: &str) -> Result<()> {
let text = read_clipboard_text().ok_or_else(|| anyhow!("clipboard is empty or unavailable"))?;
let req = paste::build_paste_request(&text)?;
let rt = RuntimeBuilder::new_current_thread().enable_all().build()?;
rt.block_on(async {
let channel = Channel::from_shared(server_addr.to_string())?
.connect()
.await?;
let mut cli = RelayClient::new(channel);
let reply = cli.paste_text(Request::new(req)).await?;
if reply.get_ref().ok {
Ok(())
} else {
Err(anyhow!("server rejected paste: {}", reply.get_ref().error))
}
})
}
#[cfg(not(coverage))]
fn read_clipboard_text() -> Option<String> {
if let Ok(out) = Command::new("sh")
.arg("-lc")
.arg(std::env::var("LESAVKA_CLIPBOARD_CMD").unwrap_or_else(
|_| "wl-paste --no-newline --type text/plain || xclip -selection clipboard -o || xsel -b -o".to_string(),
))
.output()
&& out.status.success()
{
let text = String::from_utf8_lossy(&out.stdout).to_string();
if !text.is_empty() {
return Some(text);
}
}
None
}
#[cfg(not(coverage))] #[cfg(not(coverage))]
fn set_combo_active_text(combo: &gtk::ComboBoxText, wanted: Option<&str>) { fn set_combo_active_text(combo: &gtk::ComboBoxText, wanted: Option<&str>) {
let wanted = wanted.unwrap_or("auto"); let wanted = wanted.unwrap_or("auto");
@ -419,11 +477,18 @@ fn set_combo_active_text(combo: &gtk::ComboBoxText, wanted: Option<&str>) {
} }
#[cfg(not(coverage))] #[cfg(not(coverage))]
fn spawn_client_process(server_addr: &str, state: &LauncherState) -> Result<Child> { fn spawn_client_process(
server_addr: &str,
state: &LauncherState,
input_toggle_key: &str,
) -> Result<Child> {
let exe = std::env::current_exe()?; let exe = std::env::current_exe()?;
let mut command = Command::new(exe); let mut command = Command::new(exe);
command.env("LESAVKA_LAUNCHER_CHILD", "1"); command.env("LESAVKA_LAUNCHER_CHILD", "1");
command.env("LESAVKA_SERVER_ADDR", server_addr); command.env("LESAVKA_SERVER_ADDR", server_addr);
command.env("LESAVKA_INPUT_TOGGLE_KEY", input_toggle_key);
command.env("LESAVKA_LAUNCHER_WINDOW_TITLE", "Lesavka Launcher");
command.env("LESAVKA_FOCUS_LAUNCHER_ON_LOCAL", "1");
for (key, value) in runtime_env_vars(state) { for (key, value) in runtime_env_vars(state) {
command.env(key, value); command.env(key, value);
} }
@ -445,10 +510,11 @@ fn launch_or_restart_client(
child_proc: &Rc<RefCell<Option<Child>>>, child_proc: &Rc<RefCell<Option<Child>>>,
server_addr: &str, server_addr: &str,
state: &mut LauncherState, state: &mut LauncherState,
input_toggle_key: &str,
) -> Result<()> { ) -> Result<()> {
stop_child_process(child_proc); stop_child_process(child_proc);
let _ = state.start_remote(); let _ = state.start_remote();
let child = spawn_client_process(server_addr, state)?; let child = spawn_client_process(server_addr, state, input_toggle_key)?;
*child_proc.borrow_mut() = Some(child); *child_proc.borrow_mut() = Some(child);
Ok(()) Ok(())
} }
@ -488,14 +554,6 @@ fn sync_toggle_button_labels(
}); });
} }
#[cfg(not(coverage))]
fn now_unix_seconds() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(all(test, coverage))] #[cfg(all(test, coverage))]
mod tests { mod tests {
use super::run_gui_launcher; use super::run_gui_launcher;

View File

@ -21,9 +21,9 @@
"loc": 368 "loc": 368
}, },
"client/src/input/inputs.rs": { "client/src/input/inputs.rs": {
"clippy_warnings": 40, "clippy_warnings": 42,
"doc_debt": 9, "doc_debt": 11,
"loc": 539 "loc": 550
}, },
"client/src/input/keyboard.rs": { "client/src/input/keyboard.rs": {
"clippy_warnings": 24, "clippy_warnings": 24,
@ -71,9 +71,9 @@
"loc": 234 "loc": 234
}, },
"client/src/launcher/ui.rs": { "client/src/launcher/ui.rs": {
"clippy_warnings": 4, "clippy_warnings": 6,
"doc_debt": 4, "doc_debt": 6,
"loc": 507 "loc": 565
}, },
"client/src/layout.rs": { "client/src/layout.rs": {
"clippy_warnings": 6, "clippy_warnings": 6,

View File

@ -18,7 +18,7 @@
}, },
"client/src/input/inputs.rs": { "client/src/input/inputs.rs": {
"line_percent": 97.0059880239521, "line_percent": 97.0059880239521,
"loc": 539 "loc": 550
}, },
"client/src/input/keyboard.rs": { "client/src/input/keyboard.rs": {
"line_percent": 95.27559055118111, "line_percent": 95.27559055118111,
@ -54,7 +54,7 @@
}, },
"client/src/launcher/ui.rs": { "client/src/launcher/ui.rs": {
"line_percent": 100.0, "line_percent": 100.0,
"loc": 507 "loc": 565
}, },
"client/src/layout.rs": { "client/src/layout.rs": {
"line_percent": 97.72727272727273, "line_percent": 97.72727272727273,

View File

@ -384,12 +384,16 @@ mod inputs_contract {
parse_quick_toggle_key("pause"), parse_quick_toggle_key("pause"),
Some(evdev::KeyCode::KEY_PAUSE) Some(evdev::KeyCode::KEY_PAUSE)
); );
assert_eq!(
parse_quick_toggle_key("sysrq"),
Some(evdev::KeyCode::KEY_SYSRQ)
);
assert_eq!(parse_quick_toggle_key("f12"), Some(evdev::KeyCode::KEY_F12)); assert_eq!(parse_quick_toggle_key("f12"), Some(evdev::KeyCode::KEY_F12));
assert_eq!(parse_quick_toggle_key("off"), None); assert_eq!(parse_quick_toggle_key("off"), None);
assert_eq!(parse_quick_toggle_key("none"), None); assert_eq!(parse_quick_toggle_key("none"), None);
assert_eq!( assert_eq!(
parse_quick_toggle_key("definitely-unknown"), parse_quick_toggle_key("definitely-unknown"),
Some(evdev::KeyCode::KEY_SCROLLLOCK) Some(evdev::KeyCode::KEY_PAUSE)
); );
} }
@ -397,10 +401,7 @@ mod inputs_contract {
#[serial] #[serial]
fn quick_toggle_key_env_defaults_and_respects_explicit_disable() { fn quick_toggle_key_env_defaults_and_respects_explicit_disable() {
with_var("LESAVKA_INPUT_TOGGLE_KEY", None::<&str>, || { with_var("LESAVKA_INPUT_TOGGLE_KEY", None::<&str>, || {
assert_eq!( assert_eq!(quick_toggle_key_from_env(), Some(evdev::KeyCode::KEY_PAUSE));
quick_toggle_key_from_env(),
Some(evdev::KeyCode::KEY_SCROLLLOCK)
);
}); });
with_var("LESAVKA_INPUT_TOGGLE_KEY", Some("off"), || { with_var("LESAVKA_INPUT_TOGGLE_KEY", Some("off"), || {
assert_eq!(quick_toggle_key_from_env(), None); assert_eq!(quick_toggle_key_from_env(), None);
@ -414,19 +415,13 @@ mod inputs_contract {
#[serial] #[serial]
fn quick_toggle_debounce_env_uses_defaults_and_applies_safety_floor() { fn quick_toggle_debounce_env_uses_defaults_and_applies_safety_floor() {
with_var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", None::<&str>, || { with_var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", None::<&str>, || {
assert_eq!( assert_eq!(quick_toggle_debounce_from_env(), Duration::from_millis(350));
quick_toggle_debounce_from_env(),
Duration::from_millis(350)
);
}); });
with_var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", Some("20"), || { with_var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", Some("20"), || {
assert_eq!(quick_toggle_debounce_from_env(), Duration::from_millis(50)); assert_eq!(quick_toggle_debounce_from_env(), Duration::from_millis(50));
}); });
with_var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", Some("900"), || { with_var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", Some("900"), || {
assert_eq!( assert_eq!(quick_toggle_debounce_from_env(), Duration::from_millis(900));
quick_toggle_debounce_from_env(),
Duration::from_millis(900)
);
}); });
} }