fix(launcher): bind swap keys and avoid child pid errors
This commit is contained in:
parent
dc0efc8f1a
commit
0ab5ce82ed
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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: >k::ComboBoxText) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn selected_toggle_key(combo: >k::ComboBoxText) -> String {
|
||||
combo
|
||||
.active_id()
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "pause".to_string())
|
||||
}
|
||||
|
||||
pub fn selected_toggle_key_label(combo: >k::ComboBoxText) -> String {
|
||||
combo
|
||||
.active_text()
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "Pause".to_string())
|
||||
}
|
||||
|
||||
pub fn selected_server_addr(entry: >k::Entry, fallback: &str) -> String {
|
||||
let current = entry.text();
|
||||
let trimmed = current.trim();
|
||||
@ -557,6 +549,80 @@ pub fn set_combo_active_text(combo: >k::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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user