2026-04-14 13:09:25 -03:00
use anyhow ::{ Result , anyhow } ;
2026-04-13 23:11:35 -03:00
#[ cfg(not(coverage)) ]
use {
super ::devices ::DeviceCatalog ,
2026-04-14 13:09:25 -03:00
super ::diagnostics ::quality_probe_command ,
2026-04-14 14:38:03 -03:00
super ::preview ::LauncherPreview ,
2026-04-13 23:11:35 -03:00
super ::runtime_env_vars ,
super ::state ::{ InputRouting , LauncherState , ViewMode } ,
2026-04-14 13:09:25 -03:00
crate ::paste ,
2026-04-13 23:11:35 -03:00
gtk ::prelude ::* ,
2026-04-14 13:09:25 -03:00
lesavka_common ::lesavka ::relay_client ::RelayClient ,
2026-04-13 23:11:35 -03:00
std ::cell ::RefCell ,
std ::process ::{ Child , Command } ,
std ::rc ::Rc ,
2026-04-14 13:09:25 -03:00
tokio ::runtime ::Builder as RuntimeBuilder ,
tonic ::{ Request , transport ::Channel } ,
2026-04-13 23:11:35 -03:00
} ;
#[ cfg(not(coverage)) ]
pub fn run_gui_launcher ( server_addr : String ) -> Result < ( ) > {
let app = gtk ::Application ::builder ( )
. application_id ( " dev.lesavka.launcher " )
. build ( ) ;
let catalog = Rc ::new ( DeviceCatalog ::discover ( ) ) ;
let state = Rc ::new ( RefCell ::new ( LauncherState ::new ( ) ) ) ;
state . borrow_mut ( ) . apply_catalog_defaults ( & catalog ) ;
let child_proc = Rc ::new ( RefCell ::new ( None ::< Child > ) ) ;
let server_addr = Rc ::new ( server_addr ) ;
{
let child_proc = Rc ::clone ( & child_proc ) ;
app . connect_shutdown ( move | _ | {
if let Some ( mut child ) = child_proc . borrow_mut ( ) . take ( ) {
let _ = child . kill ( ) ;
let _ = child . wait ( ) ;
}
} ) ;
}
{
let catalog = Rc ::clone ( & catalog ) ;
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let server_addr = Rc ::clone ( & server_addr ) ;
app . connect_activate ( move | app | {
let window = gtk ::ApplicationWindow ::builder ( )
. application ( app )
. title ( " Lesavka Launcher " )
2026-04-14 14:38:03 -03:00
. default_width ( 980 )
. default_height ( 860 )
2026-04-13 23:11:35 -03:00
. build ( ) ;
2026-04-14 14:38:03 -03:00
let scroll = gtk ::ScrolledWindow ::new ( ) ;
scroll . set_policy ( gtk ::PolicyType ::Never , gtk ::PolicyType ::Automatic ) ;
2026-04-13 23:11:35 -03:00
let root = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 8 ) ;
root . set_margin_start ( 14 ) ;
root . set_margin_end ( 14 ) ;
root . set_margin_top ( 14 ) ;
root . set_margin_bottom ( 14 ) ;
let heading = gtk ::Label ::new ( Some ( " Lesavka Session Launcher " ) ) ;
heading . add_css_class ( " title-2 " ) ;
heading . set_halign ( gtk ::Align ::Start ) ;
root . append ( & heading ) ;
2026-04-14 14:38:03 -03:00
let status_label = gtk ::Label ::new ( Some ( " Idle - preview warming up " ) ) ;
2026-04-13 23:11:35 -03:00
status_label . set_halign ( gtk ::Align ::Start ) ;
status_label . set_selectable ( true ) ;
root . append ( & status_label ) ;
2026-04-14 14:38:03 -03:00
let preview_frame = gtk ::Frame ::new ( Some ( " Live Preview " ) ) ;
let preview_row = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 12 ) ;
let ( left_preview , left_picture , left_status ) = build_preview_pane ( " Display 1 " ) ;
let ( right_preview , right_picture , right_status ) = build_preview_pane ( " Display 2 " ) ;
preview_row . append ( & left_preview ) ;
preview_row . append ( & right_preview ) ;
preview_frame . set_child ( Some ( & preview_row ) ) ;
root . append ( & preview_frame ) ;
2026-04-14 13:09:25 -03:00
let server_row = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
let server_label = gtk ::Label ::new ( Some ( " Server " ) ) ;
2026-04-13 23:11:35 -03:00
server_label . set_halign ( gtk ::Align ::Start ) ;
2026-04-14 13:09:25 -03:00
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 ) ;
2026-04-13 23:11:35 -03:00
let controls = gtk ::Grid ::new ( ) ;
controls . set_row_spacing ( 8 ) ;
controls . set_column_spacing ( 8 ) ;
root . append ( & controls ) ;
2026-04-14 14:38:03 -03:00
let routing_label = gtk ::Label ::new ( Some ( " Start in remote control " ) ) ;
2026-04-13 23:11:35 -03:00
routing_label . set_halign ( gtk ::Align ::Start ) ;
controls . attach ( & routing_label , 0 , 0 , 1 , 1 ) ;
let routing_switch = gtk ::Switch ::new ( ) ;
routing_switch . set_active ( matches! ( state . borrow ( ) . routing , InputRouting ::Remote ) ) ;
controls . attach ( & routing_switch , 1 , 0 , 1 , 1 ) ;
2026-04-14 14:38:03 -03:00
let view_label = gtk ::Label ::new ( Some ( " Preview mode " ) ) ;
2026-04-13 23:11:35 -03:00
view_label . set_halign ( gtk ::Align ::Start ) ;
controls . attach ( & view_label , 0 , 1 , 1 , 1 ) ;
let view_combo = gtk ::ComboBoxText ::new ( ) ;
view_combo . append ( Some ( " unified " ) , " unified " ) ;
view_combo . append ( Some ( " breakout " ) , " breakout " ) ;
view_combo . set_active ( Some ( match state . borrow ( ) . view_mode {
ViewMode ::Unified = > 0 ,
ViewMode ::Breakout = > 1 ,
} ) ) ;
controls . attach ( & view_combo , 1 , 1 , 1 , 1 ) ;
let camera_label = gtk ::Label ::new ( Some ( " Camera " ) ) ;
camera_label . set_halign ( gtk ::Align ::Start ) ;
controls . attach ( & camera_label , 0 , 2 , 1 , 1 ) ;
let camera_combo = gtk ::ComboBoxText ::new ( ) ;
camera_combo . append ( Some ( " auto " ) , " auto " ) ;
for camera in & catalog . cameras {
camera_combo . append ( Some ( camera ) , camera ) ;
}
set_combo_active_text ( & camera_combo , state . borrow ( ) . devices . camera . as_deref ( ) ) ;
controls . attach ( & camera_combo , 1 , 2 , 1 , 1 ) ;
let microphone_label = gtk ::Label ::new ( Some ( " Microphone " ) ) ;
microphone_label . set_halign ( gtk ::Align ::Start ) ;
controls . attach ( & microphone_label , 0 , 3 , 1 , 1 ) ;
let microphone_combo = gtk ::ComboBoxText ::new ( ) ;
microphone_combo . append ( Some ( " auto " ) , " auto " ) ;
for microphone in & catalog . microphones {
microphone_combo . append ( Some ( microphone ) , microphone ) ;
}
set_combo_active_text (
& microphone_combo ,
state . borrow ( ) . devices . microphone . as_deref ( ) ,
) ;
controls . attach ( & microphone_combo , 1 , 3 , 1 , 1 ) ;
let speaker_label = gtk ::Label ::new ( Some ( " Speaker " ) ) ;
speaker_label . set_halign ( gtk ::Align ::Start ) ;
controls . attach ( & speaker_label , 0 , 4 , 1 , 1 ) ;
let speaker_combo = gtk ::ComboBoxText ::new ( ) ;
speaker_combo . append ( Some ( " auto " ) , " auto " ) ;
for speaker in & catalog . speakers {
speaker_combo . append ( Some ( speaker ) , speaker ) ;
}
set_combo_active_text ( & speaker_combo , state . borrow ( ) . devices . speaker . as_deref ( ) ) ;
controls . attach ( & speaker_combo , 1 , 4 , 1 , 1 ) ;
2026-04-14 13:09:25 -03:00
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 ) ;
2026-04-13 23:11:35 -03:00
let button_row = gtk ::Box ::new ( gtk ::Orientation ::Horizontal , 8 ) ;
root . append ( & button_row ) ;
2026-04-14 14:38:03 -03:00
let start_button = gtk ::Button ::with_label ( " Start Relay " ) ;
2026-04-14 13:09:25 -03:00
let stop_button = gtk ::Button ::with_label ( " End Relay " ) ;
2026-04-14 04:02:39 -03:00
let view_toggle_button = gtk ::Button ::with_label ( " " ) ;
let input_toggle_button = gtk ::Button ::with_label ( " " ) ;
2026-04-14 13:09:25 -03:00
let clipboard_button = gtk ::Button ::with_label ( " Send Clipboard " ) ;
2026-04-13 23:11:35 -03:00
button_row . append ( & start_button ) ;
button_row . append ( & stop_button ) ;
2026-04-14 04:02:39 -03:00
button_row . append ( & view_toggle_button ) ;
button_row . append ( & input_toggle_button ) ;
2026-04-14 13:09:25 -03:00
button_row . append ( & clipboard_button ) ;
2026-04-14 04:02:39 -03:00
sync_toggle_button_labels ( & state . borrow ( ) , & view_toggle_button , & input_toggle_button ) ;
2026-04-13 23:11:35 -03:00
let probe_hint = gtk ::Label ::new ( Some ( quality_probe_command ( ) ) ) ;
probe_hint . set_halign ( gtk ::Align ::Start ) ;
probe_hint . set_selectable ( true ) ;
root . append ( & probe_hint ) ;
let note = gtk ::Label ::new ( Some (
2026-04-14 14:38:03 -03:00
" The live preview stays in this launcher by default so you can watch both displays before handing control over. Start Relay keeps the preview here in unified mode, Pop Out Windows switches back to external video windows, and Use Remote Inputs hands keyboard and mouse to the remote side. " ,
2026-04-13 23:11:35 -03:00
) ) ;
note . set_wrap ( true ) ;
note . set_halign ( gtk ::Align ::Start ) ;
root . append ( & note ) ;
2026-04-14 14:38:03 -03:00
match LauncherPreview ::new ( server_addr . as_ref ( ) . to_string ( ) ) {
Ok ( preview ) = > {
preview . install_on_picture ( 0 , & left_picture , & left_status ) ;
preview . install_on_picture ( 1 , & right_picture , & right_status ) ;
}
Err ( err ) = > {
let msg = format! ( " Preview unavailable: {err} " ) ;
left_status . set_text ( & msg ) ;
right_status . set_text ( & msg ) ;
}
}
2026-04-13 23:11:35 -03:00
{
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let status_label = status_label . clone ( ) ;
let routing_switch = routing_switch . clone ( ) ;
let view_combo = view_combo . clone ( ) ;
let camera_combo = camera_combo . clone ( ) ;
let microphone_combo = microphone_combo . clone ( ) ;
let speaker_combo = speaker_combo . clone ( ) ;
2026-04-14 13:09:25 -03:00
let toggle_key_combo = toggle_key_combo . clone ( ) ;
let server_entry = server_entry . clone ( ) ;
2026-04-13 23:11:35 -03:00
let server_addr = Rc ::clone ( & server_addr ) ;
2026-04-14 04:02:39 -03:00
let view_toggle_button = view_toggle_button . clone ( ) ;
let input_toggle_button = input_toggle_button . clone ( ) ;
2026-04-13 23:11:35 -03:00
start_button . connect_clicked ( move | _ | {
{
let mut state = state . borrow_mut ( ) ;
let routing = if routing_switch . is_active ( ) {
InputRouting ::Remote
} else {
InputRouting ::Local
} ;
state . set_routing ( routing ) ;
state . set_view_mode ( if view_combo . active ( ) = = Some ( 0 ) {
ViewMode ::Unified
} else {
ViewMode ::Breakout
} ) ;
state . select_camera ( selected_combo_value ( & camera_combo ) ) ;
state . select_microphone ( selected_combo_value ( & microphone_combo ) ) ;
state . select_speaker ( selected_combo_value ( & speaker_combo ) ) ;
}
2026-04-14 04:02:39 -03:00
sync_toggle_button_labels (
& state . borrow ( ) ,
& view_toggle_button ,
& input_toggle_button ,
) ;
2026-04-13 23:11:35 -03:00
if child_proc . borrow ( ) . is_some ( ) {
status_label . set_text ( " Session already running " ) ;
return ;
}
2026-04-14 13:09:25 -03:00
let spawn_result = relaunch_with_settings (
& child_proc ,
& state ,
& server_entry ,
server_addr . as_ref ( ) ,
& toggle_key_combo ,
) ;
2026-04-13 23:11:35 -03:00
match spawn_result {
2026-04-14 13:09:25 -03:00
Ok ( ( ) ) = > status_label . set_text ( & format! ( " Started: {} " , state . borrow ( ) . status_line ( ) ) ) ,
2026-04-13 23:11:35 -03:00
Err ( err ) = > {
let _ = state . borrow_mut ( ) . stop_remote ( ) ;
status_label . set_text ( & format! ( " Start failed: {err} " ) ) ;
}
}
} ) ;
}
{
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let status_label = status_label . clone ( ) ;
stop_button . connect_clicked ( move | _ | {
2026-04-14 04:02:39 -03:00
stop_child_process ( & child_proc ) ;
2026-04-13 23:11:35 -03:00
let _ = state . borrow_mut ( ) . stop_remote ( ) ;
2026-04-14 13:09:25 -03:00
status_label . set_text ( " Relay ended " ) ;
2026-04-13 23:11:35 -03:00
} ) ;
}
2026-04-14 04:02:39 -03:00
{
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
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 ( ) ;
2026-04-14 13:09:25 -03:00
let toggle_key_combo = toggle_key_combo . clone ( ) ;
let server_entry = server_entry . clone ( ) ;
2026-04-14 04:02:39 -03:00
let server_addr = Rc ::clone ( & server_addr ) ;
let view_toggle_button_handle = view_toggle_button . clone ( ) ;
view_toggle_button_handle . connect_clicked ( move | _ | {
{
let mut state = state . borrow_mut ( ) ;
let next = next_view_mode ( state . view_mode ) ;
state . set_view_mode ( next ) ;
let _ = view_combo . set_active_id ( Some ( state . view_mode . as_env ( ) ) ) ;
}
sync_toggle_button_labels (
& state . borrow ( ) ,
& view_toggle_button ,
& input_toggle_button ,
) ;
if child_proc . borrow ( ) . is_some ( ) {
2026-04-14 13:09:25 -03:00
let spawn_result = relaunch_with_settings (
& child_proc ,
& state ,
& server_entry ,
server_addr . as_ref ( ) ,
& toggle_key_combo ,
) ;
2026-04-14 04:02:39 -03:00
match spawn_result {
2026-04-14 13:09:25 -03:00
Ok ( ( ) ) = > status_label
. set_text ( & format! ( " View switched live: {} " , state . borrow ( ) . status_line ( ) ) ) ,
2026-04-14 04:02:39 -03:00
Err ( err ) = > {
let _ = state . borrow_mut ( ) . stop_remote ( ) ;
status_label . set_text ( & format! ( " View switch failed: {err} " ) ) ;
}
}
} else {
status_label . set_text ( & format! ( " View ready: {} " , state . borrow ( ) . status_line ( ) ) ) ;
}
} ) ;
}
{
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
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 ( ) ;
2026-04-14 13:09:25 -03:00
let toggle_key_combo = toggle_key_combo . clone ( ) ;
let server_entry = server_entry . clone ( ) ;
2026-04-14 04:02:39 -03:00
let server_addr = Rc ::clone ( & server_addr ) ;
let input_toggle_button_handle = input_toggle_button . clone ( ) ;
input_toggle_button_handle . connect_clicked ( move | _ | {
{
let mut state = state . borrow_mut ( ) ;
let next = next_input_routing ( state . routing ) ;
state . set_routing ( next ) ;
routing_switch . set_active ( matches! ( state . routing , InputRouting ::Remote ) ) ;
}
sync_toggle_button_labels (
& state . borrow ( ) ,
& view_toggle_button ,
& input_toggle_button ,
) ;
if child_proc . borrow ( ) . is_some ( ) {
2026-04-14 13:09:25 -03:00
let spawn_result = relaunch_with_settings (
& child_proc ,
& state ,
& server_entry ,
server_addr . as_ref ( ) ,
& toggle_key_combo ,
) ;
2026-04-14 04:02:39 -03:00
match spawn_result {
2026-04-14 13:09:25 -03:00
Ok ( ( ) ) = > status_label . set_text ( & format! (
" Input mode switched live: {} " ,
state . borrow ( ) . status_line ( )
) ) ,
2026-04-14 04:02:39 -03:00
Err ( err ) = > {
let _ = state . borrow_mut ( ) . stop_remote ( ) ;
status_label . set_text ( & format! ( " Input switch failed: {err} " ) ) ;
}
}
} else {
status_label . set_text ( & format! ( " Input ready: {} " , state . borrow ( ) . status_line ( ) ) ) ;
}
} ) ;
}
2026-04-13 23:11:35 -03:00
{
2026-04-14 13:09:25 -03:00
let child_proc = Rc ::clone ( & child_proc ) ;
2026-04-13 23:11:35 -03:00
let status_label = status_label . clone ( ) ;
2026-04-14 13:09:25 -03:00
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} " ) ) ,
2026-04-13 23:11:35 -03:00
}
} ) ;
}
2026-04-14 14:38:03 -03:00
scroll . set_child ( Some ( & root ) ) ;
window . set_child ( Some ( & scroll ) ) ;
2026-04-13 23:11:35 -03:00
window . present ( ) ;
} ) ;
}
2026-04-14 08:16:57 -03:00
let _ = app . run_with_args ::< & str > ( & [ ] ) ;
2026-04-13 23:11:35 -03:00
Ok ( ( ) )
}
#[ cfg(coverage) ]
pub fn run_gui_launcher ( _server_addr : String ) -> Result < ( ) > {
Ok ( ( ) )
}
#[ cfg(not(coverage)) ]
fn selected_combo_value ( combo : & gtk ::ComboBoxText ) -> Option < String > {
combo . active_text ( ) . and_then ( | value | {
let value = value . to_string ( ) ;
let trimmed = value . trim ( ) ;
if trimmed . is_empty ( ) | | trimmed . eq_ignore_ascii_case ( " auto " ) {
None
} else {
Some ( trimmed . to_string ( ) )
}
} )
}
2026-04-14 13:09:25 -03:00
#[ cfg(not(coverage)) ]
fn selected_toggle_key ( combo : & gtk ::ComboBoxText ) -> String {
combo
. active_id ( )
. map ( | value | value . to_string ( ) )
. unwrap_or_else ( | | " pause " . to_string ( ) )
}
#[ cfg(not(coverage)) ]
fn selected_server_addr ( entry : & gtk ::Entry , fallback : & str ) -> String {
let current = entry . text ( ) ;
let trimmed = current . trim ( ) ;
if trimmed . is_empty ( ) {
fallback . to_string ( )
} else {
trimmed . to_string ( )
}
}
#[ cfg(not(coverage)) ]
/// Applies the current server/key launcher controls and relaunches the child session.
fn relaunch_with_settings (
child_proc : & Rc < RefCell < Option < Child > > > ,
state : & Rc < RefCell < LauncherState > > ,
server_entry : & gtk ::Entry ,
server_fallback : & str ,
toggle_key_combo : & gtk ::ComboBoxText ,
) -> Result < ( ) > {
let server_addr = selected_server_addr ( server_entry , server_fallback ) ;
let input_toggle_key = selected_toggle_key ( toggle_key_combo ) ;
let mut state = state . borrow_mut ( ) ;
launch_or_restart_client ( child_proc , & server_addr , & mut state , & input_toggle_key )
}
#[ cfg(not(coverage)) ]
/// Reads local clipboard text and sends it to the remote server's paste RPC.
fn send_clipboard_to_remote ( server_addr : & str ) -> Result < ( ) > {
let text = read_clipboard_text ( ) . ok_or_else ( | | anyhow! ( " clipboard is empty or unavailable " ) ) ? ;
let req = paste ::build_paste_request ( & text ) ? ;
let rt = RuntimeBuilder ::new_current_thread ( ) . enable_all ( ) . build ( ) ? ;
rt . block_on ( async {
let channel = Channel ::from_shared ( server_addr . to_string ( ) ) ?
. connect ( )
. await ? ;
let mut cli = RelayClient ::new ( channel ) ;
let reply = cli . paste_text ( Request ::new ( req ) ) . await ? ;
if reply . get_ref ( ) . ok {
Ok ( ( ) )
} else {
Err ( anyhow! ( " server rejected paste: {} " , reply . get_ref ( ) . error ) )
}
} )
}
#[ cfg(not(coverage)) ]
fn read_clipboard_text ( ) -> Option < String > {
if let Ok ( out ) = Command ::new ( " sh " )
. arg ( " -lc " )
. arg ( std ::env ::var ( " LESAVKA_CLIPBOARD_CMD " ) . unwrap_or_else (
| _ | " wl-paste --no-newline --type text/plain || xclip -selection clipboard -o || xsel -b -o " . to_string ( ) ,
) )
. output ( )
& & out . status . success ( )
{
let text = String ::from_utf8_lossy ( & out . stdout ) . to_string ( ) ;
if ! text . is_empty ( ) {
return Some ( text ) ;
}
}
None
}
2026-04-13 23:11:35 -03:00
#[ cfg(not(coverage)) ]
fn set_combo_active_text ( combo : & gtk ::ComboBoxText , wanted : Option < & str > ) {
let wanted = wanted . unwrap_or ( " auto " ) ;
if ! combo . set_active_id ( Some ( wanted ) ) {
let _ = combo . set_active_id ( Some ( " auto " ) ) ;
}
}
#[ cfg(not(coverage)) ]
2026-04-14 13:09:25 -03:00
fn spawn_client_process (
server_addr : & str ,
state : & LauncherState ,
input_toggle_key : & str ,
) -> Result < Child > {
2026-04-13 23:11:35 -03:00
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 ) ;
2026-04-14 13:09:25 -03:00
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 " ) ;
2026-04-13 23:11:35 -03:00
for ( key , value ) in runtime_env_vars ( state ) {
command . env ( key , value ) ;
}
Ok ( command . spawn ( ) ? )
}
2026-04-14 04:02:39 -03:00
#[ cfg(not(coverage)) ]
/// Stops and reaps the launcher child process when one is running.
fn stop_child_process ( child_proc : & Rc < RefCell < Option < Child > > > ) {
if let Some ( mut child ) = child_proc . borrow_mut ( ) . take ( ) {
let _ = child . kill ( ) ;
let _ = child . wait ( ) ;
}
}
#[ cfg(not(coverage)) ]
/// Restarts the launcher child process with the current launcher state.
fn launch_or_restart_client (
child_proc : & Rc < RefCell < Option < Child > > > ,
server_addr : & str ,
state : & mut LauncherState ,
2026-04-14 13:09:25 -03:00
input_toggle_key : & str ,
2026-04-14 04:02:39 -03:00
) -> Result < ( ) > {
stop_child_process ( child_proc ) ;
let _ = state . start_remote ( ) ;
2026-04-14 13:09:25 -03:00
let child = spawn_client_process ( server_addr , state , input_toggle_key ) ? ;
2026-04-14 04:02:39 -03:00
* child_proc . borrow_mut ( ) = Some ( child ) ;
Ok ( ( ) )
}
#[ cfg(not(coverage)) ]
/// Flips between unified preview and breakout windows.
fn next_view_mode ( mode : ViewMode ) -> ViewMode {
match mode {
ViewMode ::Unified = > ViewMode ::Breakout ,
ViewMode ::Breakout = > ViewMode ::Unified ,
}
}
#[ cfg(not(coverage)) ]
/// Flips between remote input capture and local input passthrough.
fn next_input_routing ( routing : InputRouting ) -> InputRouting {
match routing {
InputRouting ::Remote = > InputRouting ::Local ,
InputRouting ::Local = > InputRouting ::Remote ,
}
}
#[ cfg(not(coverage)) ]
/// Keeps toggle buttons aligned with the launcher's current state.
fn sync_toggle_button_labels (
state : & LauncherState ,
view_toggle_button : & gtk ::Button ,
input_toggle_button : & gtk ::Button ,
) {
view_toggle_button . set_label ( match state . view_mode {
ViewMode ::Unified = > " Pop Out Windows " ,
ViewMode ::Breakout = > " Merge Windows " ,
} ) ;
input_toggle_button . set_label ( match state . routing {
InputRouting ::Remote = > " Use Local Inputs " ,
InputRouting ::Local = > " Use Remote Inputs " ,
} ) ;
}
2026-04-14 14:38:03 -03:00
#[ cfg(not(coverage)) ]
fn build_preview_pane ( title : & str ) -> ( gtk ::Box , gtk ::Picture , gtk ::Label ) {
let pane = gtk ::Box ::new ( gtk ::Orientation ::Vertical , 6 ) ;
pane . set_hexpand ( true ) ;
pane . set_vexpand ( true ) ;
let label = gtk ::Label ::new ( Some ( title ) ) ;
label . set_halign ( gtk ::Align ::Start ) ;
pane . append ( & label ) ;
let picture = gtk ::Picture ::new ( ) ;
picture . set_hexpand ( true ) ;
picture . set_vexpand ( true ) ;
picture . set_can_shrink ( true ) ;
picture . set_size_request ( 440 , 248 ) ;
pane . append ( & picture ) ;
let status = gtk ::Label ::new ( Some ( " Waiting for stream... " ) ) ;
status . set_halign ( gtk ::Align ::Start ) ;
pane . append ( & status ) ;
( pane , picture , status )
}
2026-04-13 23:11:35 -03:00
#[ cfg(all(test, coverage)) ]
mod tests {
use super ::run_gui_launcher ;
#[ test ]
fn coverage_stub_returns_ok ( ) {
assert! ( run_gui_launcher ( " http://127.0.0.1:50051 " . to_string ( ) ) . is_ok ( ) ) ;
}
}