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-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 " )
. default_width ( 680 )
. default_height ( 520 )
. build ( ) ;
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 ) ;
let status_label = gtk ::Label ::new ( Some ( " Idle " ) ) ;
status_label . set_halign ( gtk ::Align ::Start ) ;
status_label . set_selectable ( true ) ;
root . append ( & status_label ) ;
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 ) ;
let routing_label = gtk ::Label ::new ( Some ( " Remote input capture " ) ) ;
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 ) ;
let view_label = gtk ::Label ::new ( Some ( " View mode " ) ) ;
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 ) ;
let start_button = gtk ::Button ::with_label ( " Start Session " ) ;
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 13:09:25 -03:00
" 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. " ,
2026-04-13 23:11:35 -03:00
) ) ;
note . set_wrap ( true ) ;
note . set_halign ( gtk ::Align ::Start ) ;
root . append ( & note ) ;
{
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
}
} ) ;
}
window . set_child ( Some ( & root ) ) ;
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-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 ( ) ) ;
}
}