launcher: add pause swap key and clipboard relay controls
This commit is contained in:
parent
e61a71bd61
commit
8dd3461be0
@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@ -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: >k::ComboBoxText) -> Option<String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn selected_toggle_key(combo: >k::ComboBoxText) -> String {
|
||||||
|
combo
|
||||||
|
.active_id()
|
||||||
|
.map(|value| value.to_string())
|
||||||
|
.unwrap_or_else(|| "pause".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn selected_server_addr(entry: >k::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: >k::Entry,
|
||||||
|
server_fallback: &str,
|
||||||
|
toggle_key_combo: >k::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: >k::ComboBoxText, wanted: Option<&str>) {
|
fn set_combo_active_text(combo: >k::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: >k::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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user