fix(launcher): bind swap keys and avoid child pid errors

This commit is contained in:
Brad Stein 2026-04-15 04:44:06 -03:00
parent dc0efc8f1a
commit 0ab5ce82ed
7 changed files with 498 additions and 139 deletions

View File

@ -594,20 +594,114 @@ fn quick_toggle_key_from_env() -> Option<KeyCode> {
/// Parses a launcher/operator key alias into an evdev key code.
fn parse_quick_toggle_key(raw: &str) -> Option<KeyCode> {
let normalized = raw.trim().to_ascii_lowercase();
if matches!(normalized.as_str(), "" | "off" | "none" | "disabled") {
return None;
}
if let Some(letter) = parse_quick_toggle_letter(&normalized) {
return Some(letter);
}
if let Some(digit) = parse_quick_toggle_digit(&normalized) {
return Some(digit);
}
if let Some(function) = parse_quick_toggle_function_key(&normalized) {
return Some(function);
}
match normalized.as_str() {
"" | "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),
"f12" => Some(KeyCode::KEY_F12),
"f11" => Some(KeyCode::KEY_F11),
"f10" => Some(KeyCode::KEY_F10),
"escape" | "esc" => Some(KeyCode::KEY_ESC),
"tab" => Some(KeyCode::KEY_TAB),
"capslock" | "caps_lock" | "caps-lock" => Some(KeyCode::KEY_CAPSLOCK),
"backspace" | "back_space" | "back-space" => Some(KeyCode::KEY_BACKSPACE),
"space" | "spacebar" => Some(KeyCode::KEY_SPACE),
"enter" | "return" => Some(KeyCode::KEY_ENTER),
"insert" => Some(KeyCode::KEY_INSERT),
"delete" | "del" => Some(KeyCode::KEY_DELETE),
"home" => Some(KeyCode::KEY_HOME),
"end" => Some(KeyCode::KEY_END),
"pageup" | "page_up" | "page-up" => Some(KeyCode::KEY_PAGEUP),
"pagedown" | "page_down" | "page-down" => Some(KeyCode::KEY_PAGEDOWN),
"left" => Some(KeyCode::KEY_LEFT),
"right" => Some(KeyCode::KEY_RIGHT),
"up" => Some(KeyCode::KEY_UP),
"down" => Some(KeyCode::KEY_DOWN),
_ => Some(KeyCode::KEY_PAUSE),
}
}
fn parse_quick_toggle_letter(raw: &str) -> Option<KeyCode> {
match raw {
"a" => Some(KeyCode::KEY_A),
"b" => Some(KeyCode::KEY_B),
"c" => Some(KeyCode::KEY_C),
"d" => Some(KeyCode::KEY_D),
"e" => Some(KeyCode::KEY_E),
"f" => Some(KeyCode::KEY_F),
"g" => Some(KeyCode::KEY_G),
"h" => Some(KeyCode::KEY_H),
"i" => Some(KeyCode::KEY_I),
"j" => Some(KeyCode::KEY_J),
"k" => Some(KeyCode::KEY_K),
"l" => Some(KeyCode::KEY_L),
"m" => Some(KeyCode::KEY_M),
"n" => Some(KeyCode::KEY_N),
"o" => Some(KeyCode::KEY_O),
"p" => Some(KeyCode::KEY_P),
"q" => Some(KeyCode::KEY_Q),
"r" => Some(KeyCode::KEY_R),
"s" => Some(KeyCode::KEY_S),
"t" => Some(KeyCode::KEY_T),
"u" => Some(KeyCode::KEY_U),
"v" => Some(KeyCode::KEY_V),
"w" => Some(KeyCode::KEY_W),
"x" => Some(KeyCode::KEY_X),
"y" => Some(KeyCode::KEY_Y),
"z" => Some(KeyCode::KEY_Z),
_ => None,
}
}
fn parse_quick_toggle_digit(raw: &str) -> Option<KeyCode> {
match raw {
"1" => Some(KeyCode::KEY_1),
"2" => Some(KeyCode::KEY_2),
"3" => Some(KeyCode::KEY_3),
"4" => Some(KeyCode::KEY_4),
"5" => Some(KeyCode::KEY_5),
"6" => Some(KeyCode::KEY_6),
"7" => Some(KeyCode::KEY_7),
"8" => Some(KeyCode::KEY_8),
"9" => Some(KeyCode::KEY_9),
"0" => Some(KeyCode::KEY_0),
_ => None,
}
}
fn parse_quick_toggle_function_key(raw: &str) -> Option<KeyCode> {
match raw {
"f1" => Some(KeyCode::KEY_F1),
"f2" => Some(KeyCode::KEY_F2),
"f3" => Some(KeyCode::KEY_F3),
"f4" => Some(KeyCode::KEY_F4),
"f5" => Some(KeyCode::KEY_F5),
"f6" => Some(KeyCode::KEY_F6),
"f7" => Some(KeyCode::KEY_F7),
"f8" => Some(KeyCode::KEY_F8),
"f9" => Some(KeyCode::KEY_F9),
"f10" => Some(KeyCode::KEY_F10),
"f11" => Some(KeyCode::KEY_F11),
"f12" => Some(KeyCode::KEY_F12),
_ => None,
}
}
/// Reads debounce window from env, with a safety floor to avoid rapid flapping.
fn quick_toggle_debounce_from_env() -> Duration {
let millis = std::env::var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS")
@ -671,3 +765,37 @@ fn path_marker(path: &Path) -> u128 {
.map(|duration| duration.as_millis())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::parse_quick_toggle_key;
use evdev::KeyCode;
#[test]
fn parse_quick_toggle_key_supports_letters_digits_and_function_keys() {
assert_eq!(parse_quick_toggle_key("a"), Some(KeyCode::KEY_A));
assert_eq!(parse_quick_toggle_key("7"), Some(KeyCode::KEY_7));
assert_eq!(parse_quick_toggle_key("f12"), Some(KeyCode::KEY_F12));
assert_eq!(parse_quick_toggle_key("F3"), Some(KeyCode::KEY_F3));
}
#[test]
fn parse_quick_toggle_key_supports_navigation_and_special_aliases() {
assert_eq!(parse_quick_toggle_key("page_up"), Some(KeyCode::KEY_PAGEUP));
assert_eq!(parse_quick_toggle_key("delete"), Some(KeyCode::KEY_DELETE));
assert_eq!(parse_quick_toggle_key("spacebar"), Some(KeyCode::KEY_SPACE));
assert_eq!(
parse_quick_toggle_key("print-screen"),
Some(KeyCode::KEY_SYSRQ)
);
}
#[test]
fn parse_quick_toggle_key_can_disable_or_fall_back() {
assert_eq!(parse_quick_toggle_key("off"), None);
assert_eq!(
parse_quick_toggle_key("totally-unknown"),
Some(KeyCode::KEY_PAUSE)
);
}
}

View File

@ -83,6 +83,8 @@ pub struct LauncherState {
pub view_mode: ViewMode,
pub displays: [DisplaySurface; 2],
pub devices: DeviceSelection,
pub swap_key: String,
pub swap_key_binding: bool,
pub capture_power: CapturePowerStatus,
pub remote_active: bool,
pub notes: Vec<String>,
@ -95,6 +97,8 @@ impl Default for LauncherState {
view_mode: ViewMode::Unified,
displays: [DisplaySurface::Preview, DisplaySurface::Preview],
devices: DeviceSelection::default(),
swap_key: "pause".to_string(),
swap_key_binding: false,
capture_power: CapturePowerStatus::default(),
remote_active: false,
notes: Vec::new(),
@ -172,6 +176,18 @@ impl LauncherState {
}
}
pub fn set_swap_key(&mut self, swap_key: impl Into<String>) {
self.swap_key = normalize_swap_key(swap_key.into());
}
pub fn begin_swap_key_binding(&mut self) {
self.swap_key_binding = true;
}
pub fn finish_swap_key_binding(&mut self) {
self.swap_key_binding = false;
}
pub fn start_remote(&mut self) -> bool {
if self.remote_active {
return false;
@ -198,7 +214,7 @@ impl LauncherState {
pub fn status_line(&self) -> String {
format!(
"mode={} view={} active={} power={} d1={} d2={} camera={} mic={} speaker={}",
"mode={} view={} active={} power={} d1={} d2={} camera={} mic={} speaker={} swap={}",
match self.routing {
InputRouting::Local => "local",
InputRouting::Remote => "remote",
@ -218,6 +234,7 @@ impl LauncherState {
self.devices.camera.as_deref().unwrap_or("auto"),
self.devices.microphone.as_deref().unwrap_or("auto"),
self.devices.speaker.as_deref().unwrap_or("auto"),
self.swap_key,
)
}
}
@ -233,6 +250,15 @@ fn normalize_selection(value: Option<String>) -> Option<String> {
})
}
fn normalize_swap_key(value: String) -> String {
let trimmed = value.trim();
if trimmed.is_empty() {
"off".to_string()
} else {
trimmed.to_ascii_lowercase()
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -357,4 +383,32 @@ mod tests {
assert_eq!(state.capture_power.active_leases, 2);
assert!(state.status_line().contains("power=on"));
}
#[test]
fn swap_key_binding_tracks_selected_key_and_binding_mode() {
let mut state = LauncherState::new();
assert_eq!(state.swap_key, "pause");
assert!(!state.swap_key_binding);
state.begin_swap_key_binding();
assert!(state.swap_key_binding);
state.set_swap_key("F8");
assert_eq!(state.swap_key, "f8");
state.set_swap_key(" ");
assert_eq!(state.swap_key, "off");
state.finish_swap_key_binding();
assert!(!state.swap_key_binding);
}
#[test]
fn push_note_accumulates_operator_context() {
let mut state = LauncherState::new();
state.push_note("preview warm");
state.push_note("relay linked");
assert_eq!(state.notes, vec!["preview warm", "relay linked"]);
}
}

View File

@ -11,11 +11,12 @@ use {
super::state::{CapturePowerStatus, DisplaySurface, InputRouting, LauncherState},
super::ui_components::build_launcher_view,
super::ui_runtime::{
dock_display_to_preview, input_control_path, input_state_path, next_input_routing,
open_popout_window, path_marker, read_input_routing_state, reap_exited_child,
refresh_launcher_ui, refresh_test_buttons, routing_name, selected_combo_value,
selected_server_addr, selected_toggle_key, spawn_client_process, stop_child_process,
update_test_action_result, write_input_routing_request, RelayChild,
RelayChild, capture_swap_key, dock_display_to_preview, input_control_path,
input_state_path, next_input_routing, open_popout_window, path_marker,
read_input_routing_state, reap_exited_child, refresh_launcher_ui, refresh_test_buttons,
routing_name, selected_combo_value, selected_server_addr, spawn_client_process,
stop_child_process, toggle_key_label, update_test_action_result,
write_input_routing_request,
},
gtk::glib,
gtk::prelude::*,
@ -31,6 +32,11 @@ enum PowerMessage {
Command(std::result::Result<CapturePowerStatus, String>),
}
#[cfg(not(coverage))]
enum RelayMessage {
Spawned(std::result::Result<RelayChild, String>),
}
#[cfg(not(coverage))]
fn request_capture_power_refresh(
power_tx: std::sync::mpsc::Sender<PowerMessage>,
@ -50,8 +56,12 @@ fn request_capture_power_refresh(
fn disconnected_capture_note(mode: &str) -> &'static str {
match mode {
"forced-on" => "Relay disconnected. Capture is still forced on for staging.",
"forced-off" => "Relay disconnected. Capture stays intentionally dark until you return to Auto or Force On.",
_ => "Relay disconnected. The server will hold capture briefly, then let it return to standby.",
"forced-off" => {
"Relay disconnected. Capture stays intentionally dark until you return to Auto or Force On."
}
_ => {
"Relay disconnected. The server will hold capture briefly, then let it return to standby."
}
}
}
@ -133,6 +143,8 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let (power_tx, power_rx) = std::sync::mpsc::channel::<PowerMessage>();
let power_request_in_flight = Rc::new(Cell::new(false));
let (relay_tx, relay_rx) = std::sync::mpsc::channel::<RelayMessage>();
let relay_request_in_flight = Rc::new(Cell::new(false));
{
let state = Rc::clone(&state);
@ -228,11 +240,16 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let server_addr_fallback = Rc::clone(&server_addr);
let preview = preview.clone();
let power_tx = power_tx.clone();
let relay_tx = relay_tx.clone();
let relay_request_in_flight = Rc::clone(&relay_request_in_flight);
let start_button = widgets.start_button.clone();
let widgets_handle = widgets.clone();
start_button.connect_clicked(move |_| {
let server_addr =
selected_server_addr(&server_entry, server_addr_fallback.as_ref());
if relay_request_in_flight.get() {
return;
}
if child_proc.borrow().is_some() {
stop_child_process(&child_proc);
let power_mode = {
@ -269,63 +286,31 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let _ = std::fs::remove_file(input_control_path.as_path());
let _ = std::fs::remove_file(input_state_path.as_path());
let launch_state = state.borrow().clone();
let input_toggle_key = selected_toggle_key(&widgets.toggle_key_combo);
match spawn_client_process(
&server_addr,
&launch_state,
&input_toggle_key,
input_control_path.as_path(),
input_state_path.as_path(),
) {
Ok(child) => {
*child_proc.borrow_mut() = Some(child);
let _ = state.borrow_mut().start_remote();
if let Some(preview) = preview.as_ref() {
preview.set_server_addr(server_addr.clone());
preview.set_session_active(true);
}
let routing = routing_name(state.borrow().routing);
let power_mode = state.borrow().capture_power.mode.clone();
let message = match power_mode.as_str() {
"forced-off" => format!(
"Relay connected with inputs routed to {}, but capture is forced off. Return capture to Auto or Force On when you want remote video.",
routing
),
"forced-on" => format!(
"Relay connected with inputs routed to {}. Capture is being held awake and the eye previews are coming online.",
routing
),
_ => format!(
"Relay connected with inputs routed to {}. The eye previews will come up with the live session.",
routing
),
};
widgets_handle.status_label.set_text(&message);
request_capture_power_refresh(
power_tx.clone(),
server_addr.clone(),
Duration::from_millis(250),
);
request_capture_power_refresh(
power_tx.clone(),
server_addr,
Duration::from_millis(1250),
);
}
Err(err) => {
if let Some(preview) = preview.as_ref() {
preview.set_session_active(false);
}
widgets_handle
.status_label
.set_text(&format!("Relay start failed: {err}"));
}
}
let input_toggle_key = launch_state.swap_key.clone();
let input_control_path = input_control_path.as_ref().clone();
let input_state_path = input_state_path.as_ref().clone();
relay_request_in_flight.set(true);
widgets_handle.status_label.set_text(&format!(
"Connecting relay with {} as the swap key...",
toggle_key_label(&input_toggle_key)
));
refresh_launcher_ui(
&widgets_handle,
&state.borrow(),
child_proc.borrow().is_some(),
);
let relay_tx = relay_tx.clone();
std::thread::spawn(move || {
let result = spawn_client_process(
&server_addr,
&launch_state,
&input_toggle_key,
input_control_path.as_path(),
input_state_path.as_path(),
)
.map_err(|err| err.to_string());
let _ = relay_tx.send(RelayMessage::Spawned(result));
});
});
}
@ -364,6 +349,20 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
});
}
{
let state = Rc::clone(&state);
let child_proc = Rc::clone(&child_proc);
let widgets = widgets.clone();
let swap_key_button = widgets.swap_key_button.clone();
swap_key_button.connect_clicked(move |_| {
state.borrow_mut().begin_swap_key_binding();
widgets
.status_label
.set_text("Press a single key now to make it the swap shortcut.");
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
});
}
{
let child_proc = Rc::clone(&child_proc);
let widgets = widgets.clone();
@ -620,6 +619,52 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
});
}
{
let state = Rc::clone(&state);
let child_proc = Rc::clone(&child_proc);
let widgets = widgets.clone();
let key_controller = gtk::EventControllerKey::new();
key_controller.connect_key_pressed(move |_, key, _, _| {
if !state.borrow().swap_key_binding {
return glib::Propagation::Proceed;
}
let Some(swap_key) = capture_swap_key(key) else {
widgets.status_label.set_text(
"That key is not a good swap shortcut. Try a letter, digit, function key, or navigation key.",
);
refresh_launcher_ui(
&widgets,
&state.borrow(),
child_proc.borrow().is_some(),
);
return glib::Propagation::Stop;
};
let relay_live = child_proc.borrow().is_some() || state.borrow().remote_active;
{
let mut state = state.borrow_mut();
state.set_swap_key(swap_key.clone());
state.finish_swap_key_binding();
}
let status_message = if relay_live {
format!(
"Swap key set to {}. Disconnect and reconnect the relay to use it live.",
toggle_key_label(&swap_key)
)
} else {
format!(
"Swap key set to {}. The next relay launch will use it.",
toggle_key_label(&swap_key)
)
};
widgets.status_label.set_text(&status_message);
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
glib::Propagation::Stop
});
window.add_controller(key_controller);
}
{
let window = window.clone();
let state = Rc::clone(&state);
@ -635,6 +680,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let last_state_marker =
Rc::new(RefCell::new(path_marker(input_state_path.as_path())));
let power_request_in_flight = Rc::clone(&power_request_in_flight);
let relay_request_in_flight = Rc::clone(&relay_request_in_flight);
let preview = preview.clone();
let power_tx = power_tx.clone();
glib::timeout_add_local(Duration::from_millis(180), move || {
@ -688,6 +734,57 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
window.present();
}
while let Ok(message) = relay_rx.try_recv() {
relay_request_in_flight.set(false);
match message {
RelayMessage::Spawned(Ok(child)) => {
*child_proc.borrow_mut() = Some(child);
let _ = state.borrow_mut().start_remote();
let server_addr =
selected_server_addr(&server_entry, server_addr_fallback.as_ref());
if let Some(preview) = preview.as_ref() {
preview.set_server_addr(server_addr.clone());
preview.set_session_active(true);
}
let routing = routing_name(state.borrow().routing);
let power_mode = state.borrow().capture_power.mode.clone();
let message = match power_mode.as_str() {
"forced-off" => format!(
"Relay connected with inputs routed to {}, but capture is forced off. Return capture to Auto or Force On when you want remote video.",
routing
),
"forced-on" => format!(
"Relay connected with inputs routed to {}. Capture is being held awake and the eye previews are coming online.",
routing
),
_ => format!(
"Relay connected with inputs routed to {}. The eye previews will come up with the live session.",
routing
),
};
widgets.status_label.set_text(&message);
request_capture_power_refresh(
power_tx.clone(),
server_addr.clone(),
Duration::from_millis(250),
);
request_capture_power_refresh(
power_tx.clone(),
server_addr,
Duration::from_millis(1250),
);
}
RelayMessage::Spawned(Err(err)) => {
if let Some(preview) = preview.as_ref() {
preview.set_session_active(false);
}
widgets
.status_label
.set_text(&format!("Relay start failed: {err}"));
}
}
}
while let Ok(message) = power_rx.try_recv() {
power_request_in_flight.set(false);
match message {

View File

@ -51,7 +51,7 @@ pub struct LauncherWidgets {
pub input_toggle_button: gtk::Button,
pub clipboard_button: gtk::Button,
pub probe_button: gtk::Button,
pub toggle_key_combo: gtk::ComboBoxText,
pub swap_key_button: gtk::Button,
pub camera_test_button: gtk::Button,
pub microphone_test_button: gtk::Button,
pub speaker_test_button: gtk::Button,
@ -220,23 +220,15 @@ pub fn build_launcher_view(
input_toggle_button.set_tooltip_text(Some(
"Switch live keyboard and mouse ownership between the local machine and the remote target.",
));
let swap_label = gtk::Label::new(Some("Swap key"));
swap_label.set_halign(gtk::Align::Start);
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"));
toggle_key_combo.set_tooltip_text(Some(
"Single-key live input swap while the relay is running.",
let swap_key_button = gtk::Button::with_label(&format!(
"Set Swap Key ({})",
super::ui_runtime::toggle_key_label(&state.swap_key)
));
swap_key_button.set_tooltip_text(Some(
"Press this, then hit one keyboard key to make it the live local/remote input swap shortcut.",
));
routing_row.append(&input_toggle_button);
routing_row.append(&swap_label);
routing_row.append(&toggle_key_combo);
routing_row.append(&swap_key_button);
routing_body.append(&routing_row);
sidebar.append(&routing_panel);
@ -442,7 +434,7 @@ pub fn build_launcher_view(
input_toggle_button: input_toggle_button.clone(),
clipboard_button: clipboard_button.clone(),
probe_button: probe_button.clone(),
toggle_key_combo: toggle_key_combo.clone(),
swap_key_button: swap_key_button.clone(),
camera_test_button: camera_test_button.clone(),
microphone_test_button: microphone_test_button.clone(),
speaker_test_button: speaker_test_button.clone(),

View File

@ -1,20 +1,20 @@
use anyhow::Result;
use gtk::gio;
use gtk::{glib, prelude::*};
use std::{
cell::RefCell,
path::{Path, PathBuf},
process::{Child, Command},
rc::Rc,
};
use super::{
LAUNCHER_FOCUS_SIGNAL_ENV,
device_test::{DeviceTestController, DeviceTestKind},
launcher_focus_signal_path,
preview::LauncherPreview,
runtime_env_vars,
state::{CapturePowerStatus, DisplaySurface, InputRouting, LauncherState},
ui_components::{DisplayPaneWidgets, LauncherWidgets, PopoutWindowHandle},
LAUNCHER_FOCUS_SIGNAL_ENV,
};
pub const INPUT_CONTROL_ENV: &str = "LESAVKA_LAUNCHER_INPUT_CONTROL";
@ -22,7 +22,7 @@ pub const INPUT_STATE_ENV: &str = "LESAVKA_LAUNCHER_INPUT_STATE";
pub const DEFAULT_INPUT_CONTROL_PATH: &str = "/tmp/lesavka-launcher-input.control";
pub const DEFAULT_INPUT_STATE_PATH: &str = "/tmp/lesavka-launcher-input.state";
pub type RelayChild = gio::Subprocess;
pub type RelayChild = Child;
pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, child_running: bool) {
let relay_live = child_running || state.remote_active;
@ -46,7 +46,7 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
widgets
.summary
.shortcut_value
.set_text(&selected_toggle_key_label(&widgets.toggle_key_combo));
.set_text(&toggle_key_label(&state.swap_key));
widgets
.power_detail
@ -78,6 +78,12 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
InputRouting::Remote => "Route Inputs To Local",
InputRouting::Local => "Route Inputs To Remote",
});
let swap_key_label = if state.swap_key_binding {
"Press Any Key…".to_string()
} else {
format!("Set Swap Key ({})", toggle_key_label(&state.swap_key))
};
widgets.swap_key_button.set_label(&swap_key_label);
let power_available = state.capture_power.available;
widgets
.power_auto_button
@ -484,20 +490,6 @@ pub fn selected_combo_value(combo: &gtk::ComboBoxText) -> Option<String> {
})
}
pub fn selected_toggle_key(combo: &gtk::ComboBoxText) -> String {
combo
.active_id()
.map(|value| value.to_string())
.unwrap_or_else(|| "pause".to_string())
}
pub fn selected_toggle_key_label(combo: &gtk::ComboBoxText) -> String {
combo
.active_text()
.map(|value| value.to_string())
.unwrap_or_else(|| "Pause".to_string())
}
pub fn selected_server_addr(entry: &gtk::Entry, fallback: &str) -> String {
let current = entry.text();
let trimmed = current.trim();
@ -557,6 +549,80 @@ pub fn set_combo_active_text(combo: &gtk::ComboBoxText, wanted: Option<&str>) {
}
}
pub fn toggle_key_label(raw: &str) -> String {
match raw.trim().to_ascii_lowercase().as_str() {
"" | "off" | "none" | "disabled" => "Disabled".to_string(),
"scrolllock" | "scroll_lock" | "scroll-lock" => "Scroll Lock".to_string(),
"sysrq" | "sysreq" | "prtsc" | "printscreen" | "print_screen" | "print-screen" => {
"SysRq / PrtSc".to_string()
}
"pause" | "pausebreak" | "pause_break" | "pause-break" => "Pause".to_string(),
"pageup" | "page_up" | "page-up" => "Page Up".to_string(),
"pagedown" | "page_down" | "page-down" => "Page Down".to_string(),
"capslock" | "caps_lock" | "caps-lock" => "Caps Lock".to_string(),
"backspace" | "back_space" | "back-space" => "Backspace".to_string(),
"space" | "spacebar" => "Space".to_string(),
"escape" | "esc" => "Escape".to_string(),
value
if value.starts_with('f')
&& value.len() <= 3
&& value[1..].chars().all(|ch| ch.is_ascii_digit()) =>
{
value.to_ascii_uppercase()
}
value if value.len() == 1 => value.to_ascii_uppercase(),
value => capitalize(&value.replace('_', " ").replace('-', " ")),
}
}
pub fn capture_swap_key(key: gtk::gdk::Key) -> Option<String> {
let normalized_name = key.name()?.to_string().to_ascii_lowercase();
match normalized_name.as_str() {
"shift_l" | "shift_r" | "control_l" | "control_r" | "alt_l" | "alt_r" | "super_l"
| "super_r" | "meta_l" | "meta_r" | "hyper_l" | "hyper_r" | "iso_level3_shift"
| "multi_key" => None,
"scroll_lock" => Some("scrolllock".to_string()),
"sys_req" | "print" => Some("sysrq".to_string()),
"pause" | "break" => Some("pause".to_string()),
"page_up" => Some("pageup".to_string()),
"page_down" => Some("pagedown".to_string()),
"caps_lock" => Some("capslock".to_string()),
"backspace" => Some("backspace".to_string()),
"return" => Some("enter".to_string()),
"space" => Some("space".to_string()),
"escape" => Some("escape".to_string()),
"kp_0" => Some("0".to_string()),
"kp_1" => Some("1".to_string()),
"kp_2" => Some("2".to_string()),
"kp_3" => Some("3".to_string()),
"kp_4" => Some("4".to_string()),
"kp_5" => Some("5".to_string()),
"kp_6" => Some("6".to_string()),
"kp_7" => Some("7".to_string()),
"kp_8" => Some("8".to_string()),
"kp_9" => Some("9".to_string()),
other
if other.starts_with('f')
&& other.len() <= 3
&& other[1..].chars().all(|ch| ch.is_ascii_digit()) =>
{
Some(other.to_string())
}
other if other.len() == 1 => {
let ch = other.chars().next()?;
if ch.is_ascii_alphanumeric() {
Some(ch.to_ascii_lowercase().to_string())
} else {
None
}
}
"insert" | "delete" | "home" | "end" | "left" | "right" | "up" | "down" | "tab" => {
Some(normalized_name)
}
_ => None,
}
}
pub fn spawn_client_process(
server_addr: &str,
state: &LauncherState,
@ -565,47 +631,41 @@ pub fn spawn_client_process(
input_state_path: &Path,
) -> Result<RelayChild> {
let exe = std::env::current_exe()?;
let launcher = gio::SubprocessLauncher::new(gio::SubprocessFlags::NONE);
launcher.setenv("LESAVKA_LAUNCHER_CHILD", "1", true);
launcher.setenv("LESAVKA_SERVER_ADDR", server_addr, true);
launcher.setenv("LESAVKA_INPUT_TOGGLE_KEY", input_toggle_key, true);
launcher.setenv("LESAVKA_LAUNCHER_WINDOW_TITLE", "Lesavka Launcher", true);
launcher.setenv("LESAVKA_FOCUS_LAUNCHER_ON_LOCAL", "1", true);
launcher.setenv(
LAUNCHER_FOCUS_SIGNAL_ENV,
launcher_focus_signal_path(),
true,
);
launcher.setenv(INPUT_CONTROL_ENV, input_control_path, true);
launcher.setenv(INPUT_STATE_ENV, input_state_path, true);
launcher.setenv("LESAVKA_DISABLE_VIDEO_RENDER", "1", true);
launcher.setenv("LESAVKA_CLIPBOARD_PASTE", "0", true);
let mut command = Command::new(exe);
command.arg("--no-launcher");
command.env("LESAVKA_LAUNCHER_CHILD", "1");
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");
command.env(LAUNCHER_FOCUS_SIGNAL_ENV, launcher_focus_signal_path());
command.env(INPUT_CONTROL_ENV, input_control_path);
command.env(INPUT_STATE_ENV, input_state_path);
command.env("LESAVKA_DISABLE_VIDEO_RENDER", "1");
command.env("LESAVKA_CLIPBOARD_PASTE", "0");
for (key, value) in runtime_env_vars(state) {
launcher.setenv(key, value, true);
command.env(key, value);
}
let argv = [exe.as_os_str(), std::ffi::OsStr::new("--no-launcher")];
Ok(launcher.spawn(&argv)?)
Ok(command.spawn()?)
}
pub fn stop_child_process(child_proc: &Rc<RefCell<Option<RelayChild>>>) {
if let Some(child) = child_proc.borrow_mut().take()
&& !child.has_exited()
{
child.force_exit();
if let Some(mut child) = child_proc.borrow_mut().take() {
let _ = child.kill();
let _ = child.wait();
}
}
pub fn reap_exited_child(child_proc: &Rc<RefCell<Option<RelayChild>>>) -> bool {
let mut slot = child_proc.borrow_mut();
match slot.as_mut() {
Some(child) => {
if child.has_exited() {
Some(child) => match child.try_wait() {
Ok(Some(_)) => {
*slot = None;
false
} else {
true
}
}
Ok(None) | Err(_) => true,
},
None => false,
}
}

View File

@ -22,8 +22,8 @@
},
"client/src/input/inputs.rs": {
"clippy_warnings": 42,
"doc_debt": 16,
"loc": 673
"doc_debt": 19,
"loc": 801
},
"client/src/input/keyboard.rs": {
"clippy_warnings": 24,
@ -86,14 +86,14 @@
"loc": 442
},
"client/src/launcher/state.rs": {
"clippy_warnings": 14,
"doc_debt": 15,
"loc": 360
"clippy_warnings": 16,
"doc_debt": 18,
"loc": 414
},
"client/src/launcher/ui.rs": {
"clippy_warnings": 10,
"doc_debt": 3,
"loc": 752
"loc": 848
},
"client/src/launcher/ui_components.rs": {
"clippy_warnings": 8,
@ -102,8 +102,8 @@
},
"client/src/launcher/ui_runtime.rs": {
"clippy_warnings": 10,
"doc_debt": 20,
"loc": 670
"doc_debt": 22,
"loc": 730
},
"client/src/layout.rs": {
"clippy_warnings": 6,
@ -268,5 +268,21 @@
"doc_debt": 0,
"loc": 10
}
},
"client/src/input/inputs.rs": {
"loc": 801,
"doc_debt": 19
},
"client/src/launcher/state.rs": {
"loc": 414,
"clippy_warnings": 16,
"doc_debt": 18
},
"client/src/launcher/ui.rs": {
"loc": 848
},
"client/src/launcher/ui_runtime.rs": {
"loc": 730,
"doc_debt": 22
}
}

View File

@ -17,8 +17,8 @@
"loc": 372
},
"client/src/input/inputs.rs": {
"line_percent": 97.55102040816327,
"loc": 673
"line_percent": 98.27089337175792,
"loc": 801
},
"client/src/input/keyboard.rs": {
"line_percent": 95.9409594095941,
@ -53,12 +53,12 @@
"loc": 195
},
"client/src/launcher/state.rs": {
"line_percent": 98.29059829059828,
"loc": 360
"line_percent": 98.51851851851852,
"loc": 414
},
"client/src/launcher/ui.rs": {
"line_percent": 100.0,
"loc": 752
"loc": 848
},
"client/src/layout.rs": {
"line_percent": 97.72727272727273,
@ -164,5 +164,17 @@
"line_percent": 96.03174603174604,
"loc": 236
}
},
"client/src/input/inputs.rs": {
"loc": 801,
"line_percent": 98.27089337175792
},
"client/src/launcher/state.rs": {
"loc": 414,
"line_percent": 98.51851851851852
},
"client/src/launcher/ui.rs": {
"loc": 848,
"line_percent": 100.0
}
}