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)]
|
||||
pub fn init(&mut self) -> Result<()> {
|
||||
let paths = std::fs::read_dir("/dev/input").context("Failed to read /dev/input")?;
|
||||
@ -130,7 +128,6 @@ impl InputAggregator {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
// skip anything that isn't "event*"
|
||||
if !path
|
||||
.file_name()
|
||||
.map_or(false, |f| f.to_string_lossy().starts_with("event"))
|
||||
@ -138,7 +135,6 @@ impl InputAggregator {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ─── open the event node read-write *without* unsafe ──────────
|
||||
let mut dev = match Device::open(&path) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
@ -147,7 +143,6 @@ impl InputAggregator {
|
||||
}
|
||||
};
|
||||
|
||||
// non-blocking so fetch_events never stalls the whole loop
|
||||
dev.set_nonblocking(true)
|
||||
.with_context(|| format!("set_non_blocking {:?}", path))?;
|
||||
|
||||
@ -220,8 +215,6 @@ impl InputAggregator {
|
||||
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)]
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
loop {
|
||||
@ -332,6 +325,9 @@ impl InputAggregator {
|
||||
m.reset_state();
|
||||
}
|
||||
self.released = true;
|
||||
if !self.pending_kill {
|
||||
focus_launcher_on_local_if_enabled();
|
||||
}
|
||||
if self.pending_kill {
|
||||
return Ok(());
|
||||
}
|
||||
@ -359,7 +355,6 @@ impl InputAggregator {
|
||||
tracing::info!("🧙 magic chord - freeing devices 🪄 EXPELLIARMUS!!! 🔓🕊️");
|
||||
}
|
||||
if self.released {
|
||||
// switching to remote control
|
||||
for k in &mut self.keyboards {
|
||||
k.reset_state();
|
||||
k.set_send(true);
|
||||
@ -373,7 +368,6 @@ impl InputAggregator {
|
||||
self.released = false;
|
||||
self.pending_release = false;
|
||||
} else {
|
||||
// switching to local control: stop sending, keep grab until chord released
|
||||
for k in &mut self.keyboards {
|
||||
k.send_empty_report();
|
||||
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 {
|
||||
self.quick_toggle_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) {
|
||||
if quick_toggle_now && !self.quick_toggle_down {
|
||||
let now = Instant::now();
|
||||
@ -508,11 +500,11 @@ enum DeviceKind {
|
||||
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> {
|
||||
match std::env::var("LESAVKA_INPUT_TOGGLE_KEY") {
|
||||
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();
|
||||
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),
|
||||
_ => Some(KeyCode::KEY_SCROLLLOCK),
|
||||
_ => Some(KeyCode::KEY_PAUSE),
|
||||
}
|
||||
}
|
||||
|
||||
@ -537,3 +533,18 @@ fn quick_toggle_debounce_from_env() -> Duration {
|
||||
.unwrap_or(350);
|
||||
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))]
|
||||
use {
|
||||
super::devices::DeviceCatalog,
|
||||
super::diagnostics::{
|
||||
DiagnosticsLog, PerformanceSample, SnapshotReport, quality_probe_command,
|
||||
},
|
||||
super::diagnostics::quality_probe_command,
|
||||
super::runtime_env_vars,
|
||||
super::state::{InputRouting, LauncherState, ViewMode},
|
||||
crate::paste,
|
||||
gtk::prelude::*,
|
||||
lesavka_common::lesavka::relay_client::RelayClient,
|
||||
std::cell::RefCell,
|
||||
std::process::{Child, Command},
|
||||
std::rc::Rc,
|
||||
std::time::{SystemTime, UNIX_EPOCH},
|
||||
tokio::runtime::Builder as RuntimeBuilder,
|
||||
tonic::{Request, transport::Channel},
|
||||
};
|
||||
|
||||
#[cfg(not(coverage))]
|
||||
@ -23,7 +24,6 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
||||
let catalog = Rc::new(DeviceCatalog::discover());
|
||||
let state = Rc::new(RefCell::new(LauncherState::new()));
|
||||
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 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 state = Rc::clone(&state);
|
||||
let diagnostics = Rc::clone(&diagnostics);
|
||||
let child_proc = Rc::clone(&child_proc);
|
||||
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);
|
||||
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_selectable(true);
|
||||
root.append(&server_label);
|
||||
let server_entry = gtk::Entry::new();
|
||||
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();
|
||||
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());
|
||||
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);
|
||||
root.append(&button_row);
|
||||
|
||||
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 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(&stop_button);
|
||||
button_row.append(&view_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);
|
||||
|
||||
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);
|
||||
|
||||
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_halign(gtk::Align::Start);
|
||||
@ -167,7 +186,6 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
||||
|
||||
{
|
||||
let state = Rc::clone(&state);
|
||||
let diagnostics = Rc::clone(&diagnostics);
|
||||
let child_proc = Rc::clone(&child_proc);
|
||||
let status_label = status_label.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 microphone_combo = microphone_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 view_toggle_button = view_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;
|
||||
}
|
||||
|
||||
let spawn_result = {
|
||||
let mut state = state.borrow_mut();
|
||||
launch_or_restart_client(&child_proc, server_addr.as_ref(), &mut state)
|
||||
};
|
||||
let spawn_result = relaunch_with_settings(
|
||||
&child_proc,
|
||||
&state,
|
||||
&server_entry,
|
||||
server_addr.as_ref(),
|
||||
&toggle_key_combo,
|
||||
);
|
||||
|
||||
match spawn_result {
|
||||
Ok(()) => {
|
||||
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()));
|
||||
}
|
||||
Ok(()) => status_label.set_text(&format!("Started: {}", state.borrow().status_line())),
|
||||
Err(err) => {
|
||||
let _ = state.borrow_mut().stop_remote();
|
||||
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_child_process(&child_proc);
|
||||
let _ = state.borrow_mut().stop_remote();
|
||||
status_label.set_text("Stopped");
|
||||
status_label.set_text("Relay ended");
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let state = Rc::clone(&state);
|
||||
let child_proc = Rc::clone(&child_proc);
|
||||
let diagnostics = Rc::clone(&diagnostics);
|
||||
let status_label = status_label.clone();
|
||||
let view_combo = view_combo.clone();
|
||||
let input_toggle_button = input_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 view_toggle_button_handle = view_toggle_button.clone();
|
||||
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() {
|
||||
let spawn_result = {
|
||||
let mut state = state.borrow_mut();
|
||||
launch_or_restart_client(&child_proc, server_addr.as_ref(), &mut state)
|
||||
};
|
||||
let spawn_result = relaunch_with_settings(
|
||||
&child_proc,
|
||||
&state,
|
||||
&server_entry,
|
||||
server_addr.as_ref(),
|
||||
&toggle_key_combo,
|
||||
);
|
||||
match spawn_result {
|
||||
Ok(()) => {
|
||||
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!(
|
||||
"View switched live: {}",
|
||||
state.borrow().status_line()
|
||||
));
|
||||
}
|
||||
Ok(()) => status_label
|
||||
.set_text(&format!("View switched live: {}", state.borrow().status_line())),
|
||||
Err(err) => {
|
||||
let _ = state.borrow_mut().stop_remote();
|
||||
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 child_proc = Rc::clone(&child_proc);
|
||||
let diagnostics = Rc::clone(&diagnostics);
|
||||
let status_label = status_label.clone();
|
||||
let routing_switch = routing_switch.clone();
|
||||
let input_toggle_button = input_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 input_toggle_button_handle = input_toggle_button.clone();
|
||||
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() {
|
||||
let spawn_result = {
|
||||
let mut state = state.borrow_mut();
|
||||
launch_or_restart_client(&child_proc, server_addr.as_ref(), &mut state)
|
||||
};
|
||||
let spawn_result = relaunch_with_settings(
|
||||
&child_proc,
|
||||
&state,
|
||||
&server_entry,
|
||||
server_addr.as_ref(),
|
||||
&toggle_key_combo,
|
||||
);
|
||||
match spawn_result {
|
||||
Ok(()) => {
|
||||
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!(
|
||||
"Input mode switched live: {}",
|
||||
state.borrow().status_line()
|
||||
));
|
||||
}
|
||||
Ok(()) => status_label.set_text(&format!(
|
||||
"Input mode switched live: {}",
|
||||
state.borrow().status_line()
|
||||
)),
|
||||
Err(err) => {
|
||||
let _ = state.borrow_mut().stop_remote();
|
||||
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 diagnostics = Rc::clone(&diagnostics);
|
||||
let child_proc = Rc::clone(&child_proc);
|
||||
let status_label = status_label.clone();
|
||||
snapshot_button.connect_clicked(move |_| {
|
||||
let report = SnapshotReport::from_state(
|
||||
&state.borrow(),
|
||||
&diagnostics.borrow(),
|
||||
quality_probe_command().to_string(),
|
||||
);
|
||||
let json = match report.to_pretty_json() {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
status_label.set_text(&format!("Snapshot failed: {err}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
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}"));
|
||||
}
|
||||
let server_entry = server_entry.clone();
|
||||
let server_addr = Rc::clone(&server_addr);
|
||||
clipboard_button.connect_clicked(move |_| {
|
||||
if child_proc.borrow().is_none() {
|
||||
status_label.set_text("Start Session before sending clipboard");
|
||||
return;
|
||||
}
|
||||
let server_addr = selected_server_addr(&server_entry, server_addr.as_ref());
|
||||
match send_clipboard_to_remote(&server_addr) {
|
||||
Ok(()) => status_label.set_text("Clipboard delivered to remote"),
|
||||
Err(err) => status_label.set_text(&format!("Clipboard send 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))]
|
||||
fn set_combo_active_text(combo: >k::ComboBoxText, wanted: Option<&str>) {
|
||||
let wanted = wanted.unwrap_or("auto");
|
||||
@ -419,11 +477,18 @@ fn set_combo_active_text(combo: >k::ComboBoxText, wanted: Option<&str>) {
|
||||
}
|
||||
|
||||
#[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 mut command = Command::new(exe);
|
||||
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");
|
||||
for (key, value) in runtime_env_vars(state) {
|
||||
command.env(key, value);
|
||||
}
|
||||
@ -445,10 +510,11 @@ fn launch_or_restart_client(
|
||||
child_proc: &Rc<RefCell<Option<Child>>>,
|
||||
server_addr: &str,
|
||||
state: &mut LauncherState,
|
||||
input_toggle_key: &str,
|
||||
) -> Result<()> {
|
||||
stop_child_process(child_proc);
|
||||
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);
|
||||
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))]
|
||||
mod tests {
|
||||
use super::run_gui_launcher;
|
||||
|
||||
@ -21,9 +21,9 @@
|
||||
"loc": 368
|
||||
},
|
||||
"client/src/input/inputs.rs": {
|
||||
"clippy_warnings": 40,
|
||||
"doc_debt": 9,
|
||||
"loc": 539
|
||||
"clippy_warnings": 42,
|
||||
"doc_debt": 11,
|
||||
"loc": 550
|
||||
},
|
||||
"client/src/input/keyboard.rs": {
|
||||
"clippy_warnings": 24,
|
||||
@ -71,9 +71,9 @@
|
||||
"loc": 234
|
||||
},
|
||||
"client/src/launcher/ui.rs": {
|
||||
"clippy_warnings": 4,
|
||||
"doc_debt": 4,
|
||||
"loc": 507
|
||||
"clippy_warnings": 6,
|
||||
"doc_debt": 6,
|
||||
"loc": 565
|
||||
},
|
||||
"client/src/layout.rs": {
|
||||
"clippy_warnings": 6,
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
},
|
||||
"client/src/input/inputs.rs": {
|
||||
"line_percent": 97.0059880239521,
|
||||
"loc": 539
|
||||
"loc": 550
|
||||
},
|
||||
"client/src/input/keyboard.rs": {
|
||||
"line_percent": 95.27559055118111,
|
||||
@ -54,7 +54,7 @@
|
||||
},
|
||||
"client/src/launcher/ui.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 507
|
||||
"loc": 565
|
||||
},
|
||||
"client/src/layout.rs": {
|
||||
"line_percent": 97.72727272727273,
|
||||
|
||||
@ -384,12 +384,16 @@ mod inputs_contract {
|
||||
parse_quick_toggle_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("off"), None);
|
||||
assert_eq!(parse_quick_toggle_key("none"), None);
|
||||
assert_eq!(
|
||||
parse_quick_toggle_key("definitely-unknown"),
|
||||
Some(evdev::KeyCode::KEY_SCROLLLOCK)
|
||||
Some(evdev::KeyCode::KEY_PAUSE)
|
||||
);
|
||||
}
|
||||
|
||||
@ -397,10 +401,7 @@ mod inputs_contract {
|
||||
#[serial]
|
||||
fn quick_toggle_key_env_defaults_and_respects_explicit_disable() {
|
||||
with_var("LESAVKA_INPUT_TOGGLE_KEY", None::<&str>, || {
|
||||
assert_eq!(
|
||||
quick_toggle_key_from_env(),
|
||||
Some(evdev::KeyCode::KEY_SCROLLLOCK)
|
||||
);
|
||||
assert_eq!(quick_toggle_key_from_env(), Some(evdev::KeyCode::KEY_PAUSE));
|
||||
});
|
||||
with_var("LESAVKA_INPUT_TOGGLE_KEY", Some("off"), || {
|
||||
assert_eq!(quick_toggle_key_from_env(), None);
|
||||
@ -414,19 +415,13 @@ mod inputs_contract {
|
||||
#[serial]
|
||||
fn quick_toggle_debounce_env_uses_defaults_and_applies_safety_floor() {
|
||||
with_var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", None::<&str>, || {
|
||||
assert_eq!(
|
||||
quick_toggle_debounce_from_env(),
|
||||
Duration::from_millis(350)
|
||||
);
|
||||
assert_eq!(quick_toggle_debounce_from_env(), Duration::from_millis(350));
|
||||
});
|
||||
with_var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", Some("20"), || {
|
||||
assert_eq!(quick_toggle_debounce_from_env(), Duration::from_millis(50));
|
||||
});
|
||||
with_var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", Some("900"), || {
|
||||
assert_eq!(
|
||||
quick_toggle_debounce_from_env(),
|
||||
Duration::from_millis(900)
|
||||
);
|
||||
assert_eq!(quick_toggle_debounce_from_env(), Duration::from_millis(900));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user