2026-04-14 18:44:40 -03:00
use anyhow ::Result ;
2026-04-13 23:11:35 -03:00
#[ cfg(not(coverage)) ]
use {
2026-04-16 12:58:05 -03:00
super ::clipboard ::send_clipboard_text_to_remote ,
2026-04-15 01:57:14 -03:00
super ::device_test ::{ DeviceTestController , DeviceTestKind } ,
2026-04-13 23:11:35 -03:00
super ::devices ::DeviceCatalog ,
2026-04-16 21:18:34 -03:00
super ::diagnostics ::{ PerformanceSample , quality_probe_command } ,
2026-04-16 12:58:05 -03:00
super ::launcher_clipboard_control_path ,
2026-04-14 18:44:40 -03:00
super ::launcher_focus_signal_path ,
2026-04-20 08:38:26 -03:00
super ::power ::{ fetch_capture_power , reset_usb_gadget , set_capture_power_mode } ,
2026-04-16 12:58:05 -03:00
super ::state ::{
2026-04-19 03:28:23 -03:00
BreakoutSizePreset , CapturePowerStatus , CaptureSizePreset , DisplaySurface ,
FeedSourcePreset , InputRouting , LauncherState ,
2026-04-16 12:58:05 -03:00
} ,
2026-04-20 11:11:51 -03:00
super ::ui_components ::{ build_launcher_view , sync_input_device_combo , sync_stage_device_combo } ,
2026-04-14 23:03:18 -03:00
super ::ui_runtime ::{
2026-04-16 12:58:05 -03:00
RelayChild , append_session_log , apply_popout_window_size , attach_child_log_streams ,
2026-04-20 12:12:29 -03:00
capture_swap_key , copy_plain_text , copy_session_log , dock_all_displays_to_preview ,
dock_display_to_preview , input_control_path , input_state_path , next_input_routing ,
open_diagnostics_popout , open_popout_window , open_session_log_popout , path_marker ,
present_popout_windows , read_input_routing_state , reap_exited_child , refresh_launcher_ui ,
refresh_test_buttons , routing_name , selected_combo_value , selected_server_addr ,
shutdown_launcher_runtime , spawn_client_process , stop_child_process , toggle_key_label ,
update_test_action_result , write_input_routing_request ,
2026-04-14 23:03:18 -03:00
} ,
2026-04-16 21:18:34 -03:00
crate ::handshake ::{ HandshakeProbe , probe } ,
2026-04-16 12:58:05 -03:00
crate ::output ::display ::enumerate_monitors ,
2026-04-14 18:44:40 -03:00
gtk ::glib ,
2026-04-14 20:05:26 -03:00
gtk ::prelude ::* ,
2026-04-15 01:20:51 -03:00
lesavka_common ::lesavka ::CapturePowerCommand ,
2026-04-17 06:14:54 -03:00
lesavka_common ::process_metrics ::ProcessCpuSampler ,
2026-04-14 23:03:18 -03:00
std ::cell ::{ Cell , RefCell } ,
2026-04-16 21:18:34 -03:00
std ::collections ::VecDeque ,
2026-04-16 12:58:05 -03:00
std ::process ::Command ,
2026-04-13 23:11:35 -03:00
std ::rc ::Rc ,
2026-04-16 21:18:34 -03:00
std ::time ::{ Duration , Instant } ,
2026-04-13 23:11:35 -03:00
} ;
2026-04-14 20:05:26 -03:00
#[ cfg(not(coverage)) ]
2026-04-14 23:03:18 -03:00
enum PowerMessage {
2026-04-15 04:11:47 -03:00
Refresh ( std ::result ::Result < CapturePowerStatus , String > ) ,
2026-04-14 23:03:18 -03:00
Command ( std ::result ::Result < CapturePowerStatus , String > ) ,
2026-04-14 20:05:26 -03:00
}
2026-04-15 04:44:06 -03:00
#[ cfg(not(coverage)) ]
enum RelayMessage {
Spawned ( std ::result ::Result < RelayChild , String > ) ,
}
2026-04-16 12:58:05 -03:00
#[ cfg(not(coverage)) ]
enum CapsMessage {
2026-04-16 21:18:34 -03:00
Refresh ( HandshakeProbe ) ,
2026-04-16 12:58:05 -03:00
}
#[ cfg(not(coverage)) ]
enum ClipboardMessage {
Finished ( std ::result ::Result < String , String > ) ,
}
2026-04-16 21:18:34 -03:00
#[ cfg(not(coverage)) ]
const NETWORK_TELEMETRY_WINDOW : Duration = Duration ::from_secs ( 8 ) ;
2026-04-20 13:59:34 -03:00
#[ cfg(not(coverage)) ]
fn usb_audio_kernel_support_missing ( ) -> bool {
Command ::new ( " modinfo " )
. arg ( " snd_usb_audio " )
. status ( )
. map ( | status | ! status . success ( ) )
. unwrap_or ( true )
}
2026-04-16 21:18:34 -03:00
#[ cfg(not(coverage)) ]
#[ derive(Default) ]
struct NetworkTelemetry {
rtt_samples : VecDeque < ( Instant , f32 ) > ,
failures : VecDeque < Instant > ,
}
#[ cfg(not(coverage)) ]
#[ derive(Clone, Copy, Debug, Default) ]
struct NetworkSnapshot {
rtt_ms : f32 ,
2026-04-17 01:09:33 -03:00
probe_spread_ms : f32 ,
2026-04-16 21:18:34 -03:00
probe_loss_pct : f32 ,
}
#[ cfg(not(coverage)) ]
impl NetworkTelemetry {
fn record ( & mut self , probe : & HandshakeProbe ) {
let now = Instant ::now ( ) ;
self . trim ( now ) ;
if let Some ( rtt_ms ) = probe . rtt_ms {
self . rtt_samples . push_back ( ( now , rtt_ms ) ) ;
} else {
self . failures . push_back ( now ) ;
}
self . trim ( now ) ;
}
fn snapshot ( & mut self ) -> NetworkSnapshot {
let now = Instant ::now ( ) ;
self . trim ( now ) ;
let rtt_ms = self . rtt_samples . back ( ) . map ( | ( _ , rtt ) | * rtt ) . unwrap_or ( 0.0 ) ;
2026-04-17 01:09:33 -03:00
let probe_spread_ms = network_spread_ms ( & self . rtt_samples ) ;
2026-04-16 21:18:34 -03:00
let probe_count = self . rtt_samples . len ( ) + self . failures . len ( ) ;
let probe_loss_pct = if probe_count = = 0 {
0.0
} else {
self . failures . len ( ) as f32 * 100.0 / probe_count as f32
} ;
NetworkSnapshot {
rtt_ms ,
2026-04-17 01:09:33 -03:00
probe_spread_ms ,
2026-04-16 21:18:34 -03:00
probe_loss_pct ,
}
}
fn trim ( & mut self , now : Instant ) {
while let Some ( ( oldest , _ ) ) = self . rtt_samples . front ( ) . copied ( ) {
if now . saturating_duration_since ( oldest ) > NETWORK_TELEMETRY_WINDOW {
let _ = self . rtt_samples . pop_front ( ) ;
} else {
break ;
}
}
while let Some ( oldest ) = self . failures . front ( ) . copied ( ) {
if now . saturating_duration_since ( oldest ) > NETWORK_TELEMETRY_WINDOW {
let _ = self . failures . pop_front ( ) ;
} else {
break ;
}
}
}
}
2026-04-20 11:11:51 -03:00
#[ cfg(not(coverage)) ]
fn retained_stage_selection ( current : Option < & str > , values : & [ String ] ) -> Option < String > {
current
. filter ( | selected | values . iter ( ) . any ( | value | value = = * selected ) )
. map ( str ::to_string )
. or_else ( | | values . first ( ) . cloned ( ) )
}
#[ cfg(not(coverage)) ]
fn retained_input_selection ( current : Option < & str > , values : & [ String ] ) -> Option < String > {
current
. filter ( | selected | values . iter ( ) . any ( | value | value = = * selected ) )
. map ( str ::to_string )
}
2026-04-16 21:18:34 -03:00
#[ cfg(not(coverage)) ]
2026-04-17 01:09:33 -03:00
fn network_spread_ms ( samples : & VecDeque < ( Instant , f32 ) > ) -> f32 {
2026-04-16 21:18:34 -03:00
if samples . len ( ) < 2 {
return 0.0 ;
}
2026-04-17 01:09:33 -03:00
let mut values = samples . iter ( ) . map ( | ( _ , value ) | * value ) . collect ::< Vec < _ > > ( ) ;
values . sort_by ( | a , b | a . partial_cmp ( b ) . unwrap_or ( std ::cmp ::Ordering ::Equal ) ) ;
let median = values [ values . len ( ) / 2 ] ;
let mut deviations = values
. into_iter ( )
. map ( | value | ( value - median ) . abs ( ) )
. collect ::< Vec < _ > > ( ) ;
deviations . sort_by ( | a , b | a . partial_cmp ( b ) . unwrap_or ( std ::cmp ::Ordering ::Equal ) ) ;
deviations [ deviations . len ( ) / 2 ]
2026-04-16 21:18:34 -03:00
}
2026-04-15 04:11:47 -03:00
#[ cfg(not(coverage)) ]
fn request_capture_power_refresh (
power_tx : std ::sync ::mpsc ::Sender < PowerMessage > ,
server_addr : String ,
delay : Duration ,
) {
std ::thread ::spawn ( move | | {
if ! delay . is_zero ( ) {
std ::thread ::sleep ( delay ) ;
}
let result = fetch_capture_power ( & server_addr ) . map_err ( | err | err . to_string ( ) ) ;
let _ = power_tx . send ( PowerMessage ::Refresh ( result ) ) ;
} ) ;
}
2026-04-16 12:58:05 -03:00
#[ cfg(not(coverage)) ]
fn request_capture_power_command (
power_tx : std ::sync ::mpsc ::Sender < PowerMessage > ,
server_addr : String ,
command : CapturePowerCommand ,
) {
std ::thread ::spawn ( move | | {
let result = set_capture_power_mode ( & server_addr , command ) . map_err ( | err | err . to_string ( ) ) ;
let _ = power_tx . send ( PowerMessage ::Command ( result ) ) ;
} ) ;
}
#[ cfg(not(coverage)) ]
fn request_handshake_caps (
caps_tx : std ::sync ::mpsc ::Sender < CapsMessage > ,
server_addr : String ,
delay : Duration ,
) {
std ::thread ::spawn ( move | | {
if ! delay . is_zero ( ) {
std ::thread ::sleep ( delay ) ;
}
let runtime = tokio ::runtime ::Builder ::new_current_thread ( )
. enable_all ( )
. build ( ) ;
2026-04-16 21:18:34 -03:00
let probe = match runtime {
Ok ( runtime ) = > runtime . block_on ( probe ( & server_addr ) ) ,
Err ( _ ) = > HandshakeProbe ::default ( ) ,
2026-04-16 12:58:05 -03:00
} ;
2026-04-16 21:18:34 -03:00
let _ = caps_tx . send ( CapsMessage ::Refresh ( probe ) ) ;
2026-04-16 12:58:05 -03:00
} ) ;
}
#[ cfg(not(coverage)) ]
fn unavailable_capture_power ( detail : String ) -> CapturePowerStatus {
CapturePowerStatus {
available : false ,
enabled : false ,
unit : " relay.service " . to_string ( ) ,
detail ,
active_leases : 0 ,
mode : " auto " . to_string ( ) ,
2026-04-20 01:06:50 -03:00
detected_devices : 0 ,
2026-04-16 12:58:05 -03:00
}
}
#[ cfg(not(coverage)) ]
fn refresh_eye_feed_controls (
widgets : & super ::ui_components ::LauncherWidgets ,
state : & LauncherState ,
) {
for monitor_id in 0 .. 2 {
2026-04-19 03:28:23 -03:00
super ::ui_components ::sync_feed_source_combo (
& widgets . display_panes [ monitor_id ] . feed_source_combo ,
state . feed_source_options ( monitor_id ) ,
state . feed_source_preset ( monitor_id ) ,
2026-04-17 01:09:33 -03:00
) ;
2026-04-19 03:28:23 -03:00
if state . feed_source_preset ( monitor_id ) ! = FeedSourcePreset ::Off {
2026-04-19 11:42:41 -03:00
let choice = state
. display_capture_size_choice ( monitor_id )
. unwrap_or_else ( | | state . capture_size_choice ( monitor_id ) ) ;
if state . feed_source_preset ( monitor_id ) = = FeedSourcePreset ::ThisEye {
super ::ui_components ::sync_capture_resolution_combo (
& widgets . display_panes [ monitor_id ] . capture_resolution_combo ,
state . capture_size_options ( ) ,
state . capture_size_preset ( monitor_id ) ,
) ;
} else {
super ::ui_components ::sync_capture_resolution_locked (
& widgets . display_panes [ monitor_id ] . capture_resolution_combo ,
state . capture_size_options ( ) ,
choice . preset ,
) ;
}
2026-04-19 03:28:23 -03:00
} else {
super ::ui_components ::sync_capture_resolution_disabled (
& widgets . display_panes [ monitor_id ] . capture_resolution_combo ,
) ;
}
2026-04-16 12:58:05 -03:00
super ::ui_components ::sync_breakout_size_combo (
& widgets . display_panes [ monitor_id ] . breakout_combo ,
2026-04-19 14:14:14 -03:00
state . breakout_size_options ( monitor_id ) ,
2026-04-16 12:58:05 -03:00
state . breakout_size_preset ( monitor_id ) ,
) ;
2026-04-19 15:07:24 -03:00
refresh_preview_frame_ratio ( widgets , monitor_id , state ) ;
2026-04-16 12:58:05 -03:00
}
}
2026-04-19 15:07:24 -03:00
#[ cfg(not(coverage)) ]
fn refresh_preview_frame_ratio (
widgets : & super ::ui_components ::LauncherWidgets ,
monitor_id : usize ,
state : & LauncherState ,
) {
let capture = state
. display_capture_size_choice ( monitor_id )
. unwrap_or_else ( | | state . capture_size_choice ( monitor_id ) ) ;
widgets . display_panes [ monitor_id ]
. preview_frame
. set_ratio ( capture . preset . display_aspect_ratio ( ) ) ;
}
2026-04-16 21:36:16 -03:00
#[ cfg(not(coverage)) ]
fn eye_caps_changed ( state : & LauncherState , caps : & crate ::handshake ::PeerCaps ) -> bool {
let next_width = caps . eye_width . unwrap_or ( state . preview_source . width ) ;
let next_height = caps . eye_height . unwrap_or ( state . preview_source . height ) ;
let next_fps = caps . eye_fps . unwrap_or ( state . preview_source . fps ) ;
state . preview_source . width ! = next_width
| | state . preview_source . height ! = next_height
| | state . preview_source . fps ! = next_fps
}
2026-04-16 21:18:34 -03:00
#[ cfg(not(coverage)) ]
fn record_diagnostics_sample (
widgets : & super ::ui_components ::LauncherWidgets ,
state : & LauncherState ,
preview : Option < & super ::preview ::LauncherPreview > ,
network : NetworkSnapshot ,
2026-04-17 06:14:54 -03:00
client_process_cpu_pct : f32 ,
2026-04-16 21:18:34 -03:00
) {
let left_metrics = preview
. and_then ( | preview | {
2026-04-19 03:28:23 -03:00
( state . feed_source_preset ( 0 ) ! = FeedSourcePreset ::Off ) . then_some (
preview . snapshot_metrics (
0 ,
match state . display_surface ( 0 ) {
DisplaySurface ::Preview = > super ::preview ::PreviewSurface ::Inline ,
DisplaySurface ::Window = > super ::preview ::PreviewSurface ::Window ,
} ,
) ,
2026-04-16 21:18:34 -03:00
)
} )
2026-04-19 03:28:23 -03:00
. flatten ( )
2026-04-16 21:18:34 -03:00
. unwrap_or_default ( ) ;
let right_metrics = preview
. and_then ( | preview | {
2026-04-19 03:28:23 -03:00
( state . feed_source_preset ( 1 ) ! = FeedSourcePreset ::Off ) . then_some (
preview . snapshot_metrics (
1 ,
match state . display_surface ( 1 ) {
DisplaySurface ::Preview = > super ::preview ::PreviewSurface ::Inline ,
DisplaySurface ::Window = > super ::preview ::PreviewSurface ::Window ,
} ,
) ,
2026-04-16 21:18:34 -03:00
)
} )
2026-04-19 03:28:23 -03:00
. flatten ( )
2026-04-16 21:18:34 -03:00
. unwrap_or_default ( ) ;
widgets
. diagnostics_log
. borrow_mut ( )
. record ( PerformanceSample {
rtt_ms : network . rtt_ms ,
2026-04-17 01:09:33 -03:00
probe_spread_ms : network . probe_spread_ms ,
2026-04-16 21:18:34 -03:00
input_latency_ms : network . rtt_ms * 0.5 ,
probe_loss_pct : network . probe_loss_pct ,
2026-04-17 06:14:54 -03:00
client_process_cpu_pct ,
server_process_cpu_pct : left_metrics
. server_process_cpu_pct
. max ( right_metrics . server_process_cpu_pct ) ,
2026-04-16 21:18:34 -03:00
video_loss_pct : left_metrics
. packet_loss_pct
. max ( right_metrics . packet_loss_pct ) ,
left_receive_fps : left_metrics . receive_fps ,
left_present_fps : left_metrics . present_fps ,
left_server_fps : left_metrics . server_fps ,
2026-04-17 05:50:24 -03:00
left_stream_spread_ms : left_metrics . stream_spread_ms ,
left_packet_gap_peak_ms : left_metrics . packet_gap_peak_ms ,
left_present_gap_peak_ms : left_metrics . present_gap_peak_ms ,
left_queue_depth : left_metrics . queue_depth ,
left_queue_peak : left_metrics . queue_depth_peak ,
2026-04-17 05:59:56 -03:00
left_server_source_gap_peak_ms : left_metrics . server_source_gap_peak_ms ,
left_server_send_gap_peak_ms : left_metrics . server_send_gap_peak_ms ,
left_server_queue_peak : left_metrics . server_queue_peak ,
2026-04-17 06:14:54 -03:00
left_server_encoder_label : left_metrics . server_encoder_label . clone ( ) ,
2026-04-17 05:50:24 -03:00
left_decoder_label : left_metrics . decoder_label . clone ( ) ,
2026-04-18 02:12:01 -03:00
left_stream_caps_label : left_metrics . stream_caps_label . clone ( ) ,
left_decoded_caps_label : left_metrics . decoded_caps_label . clone ( ) ,
2026-04-19 03:28:23 -03:00
left_rendered_caps_label : left_metrics . rendered_caps_label . clone ( ) ,
2026-04-16 21:18:34 -03:00
right_receive_fps : right_metrics . receive_fps ,
right_present_fps : right_metrics . present_fps ,
right_server_fps : right_metrics . server_fps ,
2026-04-17 05:50:24 -03:00
right_stream_spread_ms : right_metrics . stream_spread_ms ,
right_packet_gap_peak_ms : right_metrics . packet_gap_peak_ms ,
right_present_gap_peak_ms : right_metrics . present_gap_peak_ms ,
right_queue_depth : right_metrics . queue_depth ,
right_queue_peak : right_metrics . queue_depth_peak ,
2026-04-17 05:59:56 -03:00
right_server_source_gap_peak_ms : right_metrics . server_source_gap_peak_ms ,
right_server_send_gap_peak_ms : right_metrics . server_send_gap_peak_ms ,
right_server_queue_peak : right_metrics . server_queue_peak ,
2026-04-17 06:14:54 -03:00
right_server_encoder_label : right_metrics . server_encoder_label . clone ( ) ,
2026-04-17 05:50:24 -03:00
right_decoder_label : right_metrics . decoder_label . clone ( ) ,
2026-04-18 02:12:01 -03:00
right_stream_caps_label : right_metrics . stream_caps_label . clone ( ) ,
right_decoded_caps_label : right_metrics . decoded_caps_label . clone ( ) ,
2026-04-19 03:28:23 -03:00
right_rendered_caps_label : right_metrics . rendered_caps_label . clone ( ) ,
2026-04-16 21:18:34 -03:00
dropped_frames : left_metrics
. dropped_frames
. saturating_add ( right_metrics . dropped_frames ) ,
queue_depth : left_metrics . queue_depth . max ( right_metrics . queue_depth ) ,
} ) ;
}
2026-04-16 12:58:05 -03:00
#[ cfg(not(coverage)) ]
fn largest_monitor_size ( ) -> ( u32 , u32 ) {
let ( width , height ) = enumerate_monitors ( )
. into_iter ( )
. max_by_key ( | monitor | {
effective_monitor_width ( & monitor ) as u64 * effective_monitor_height ( & monitor ) as u64
} )
. map ( | monitor | {
(
effective_monitor_width ( & monitor ) ,
effective_monitor_height ( & monitor ) ,
)
} )
. unwrap_or ( ( 1920 , 1080 ) ) ;
( width . max ( 2 ) , height . max ( 2 ) )
}
#[ cfg(not(coverage)) ]
fn largest_monitor_physical_size ( ) -> ( u32 , u32 ) {
if let Some ( ( width , height ) ) = probe_kscreen_display_size ( ) {
return ( width , height ) ;
}
normalize_breakout_limit ( largest_monitor_size ( ) . 0 , largest_monitor_size ( ) . 1 )
}
#[ cfg(not(coverage)) ]
fn probe_kscreen_display_size ( ) -> Option < ( u32 , u32 ) > {
let output = Command ::new ( " kscreen-doctor " ) . arg ( " -o " ) . output ( ) . ok ( ) ? ;
if ! output . status . success ( ) {
return None ;
}
let text = String ::from_utf8 ( output . stdout ) . ok ( ) ? ;
let mut best = None ;
for line in text . lines ( ) {
if ! line . contains ( " Modes: " ) {
continue ;
}
let active = line
. split_whitespace ( )
. find ( | token | token . contains ( '*' ) & & token . contains ( 'x' ) ) ? ;
let dims = active
. trim_matches ( | ch : char | ch = = '*' | | ch = = '!' )
. split ( '@' )
. next ( ) ? ;
let ( width , height ) = dims . split_once ( 'x' ) ? ;
let width = width . parse ::< u32 > ( ) . ok ( ) ? ;
let height = height . parse ::< u32 > ( ) . ok ( ) ? ;
if best
. map ( | ( best_w , best_h ) | width as u64 * height as u64 > best_w as u64 * best_h as u64 )
. unwrap_or ( true )
{
best = Some ( ( width , height ) ) ;
}
}
best
}
#[ cfg(not(coverage)) ]
fn effective_monitor_width ( monitor : & crate ::output ::display ::MonitorInfo ) -> u32 {
let scale = monitor . scale_factor . max ( 1 ) as u32 ;
( monitor . geometry . width ( ) . max ( 1 ) as u32 ) . saturating_mul ( scale )
}
#[ cfg(not(coverage)) ]
fn effective_monitor_height ( monitor : & crate ::output ::display ::MonitorInfo ) -> u32 {
let scale = monitor . scale_factor . max ( 1 ) as u32 ;
( monitor . geometry . height ( ) . max ( 1 ) as u32 ) . saturating_mul ( scale )
}
#[ cfg(not(coverage)) ]
fn normalize_breakout_limit ( width : u32 , height : u32 ) -> ( u32 , u32 ) {
const STANDARD_SIZES : & [ ( u32 , u32 ) ] = & [
( 3840 , 2160 ) ,
( 2560 , 1440 ) ,
( 1920 , 1080 ) ,
( 1600 , 900 ) ,
( 1366 , 768 ) ,
( 1280 , 720 ) ,
( 960 , 540 ) ,
] ;
STANDARD_SIZES
. iter ( )
. copied ( )
. find ( | ( candidate_w , candidate_h ) | * candidate_w < = width & & * candidate_h < = height )
. unwrap_or ( ( width . max ( 2 ) , height . max ( 2 ) ) )
}
2026-04-19 04:24:27 -03:00
#[ cfg(not(coverage)) ]
fn launcher_default_size ( width : u32 , height : u32 ) -> ( i32 , i32 ) {
let max_width = width . saturating_sub ( 48 ) . max ( 640 ) as i32 ;
let max_height = height . saturating_sub ( 72 ) . max ( 520 ) as i32 ;
( 1380. min ( max_width ) , 860. min ( max_height ) )
}
2026-04-16 12:58:05 -03:00
#[ cfg(not(coverage)) ]
fn rebind_inline_preview (
preview : & super ::preview ::LauncherPreview ,
widgets : & super ::ui_components ::LauncherWidgets ,
2026-04-19 03:28:23 -03:00
state : & LauncherState ,
2026-04-16 12:58:05 -03:00
monitor_id : usize ,
) {
if let Some ( binding ) = widgets . display_panes [ monitor_id ]
. preview_binding
. borrow_mut ( )
. take ( )
{
binding . close ( ) ;
}
2026-04-19 03:28:23 -03:00
if state . feed_source_preset ( monitor_id ) = = FeedSourcePreset ::Off {
widgets . display_panes [ monitor_id ]
. picture
. set_paintable ( Option ::< & gtk ::gdk ::Paintable > ::None ) ;
widgets . display_panes [ monitor_id ]
. stream_status
. set_text ( " Feed disabled. " ) ;
return ;
}
2026-04-16 12:58:05 -03:00
let binding = preview . install_on_picture (
monitor_id ,
super ::preview ::PreviewSurface ::Inline ,
& widgets . display_panes [ monitor_id ] . picture ,
& widgets . display_panes [ monitor_id ] . stream_status ,
) ;
* widgets . display_panes [ monitor_id ]
. preview_binding
. borrow_mut ( ) = binding ;
}
#[ cfg(not(coverage)) ]
fn rebind_popout_preview (
preview : & super ::preview ::LauncherPreview ,
popouts : & Rc < RefCell < [ Option < super ::ui_components ::PopoutWindowHandle > ; 2 ] > > ,
2026-04-19 03:28:23 -03:00
state : & LauncherState ,
2026-04-16 12:58:05 -03:00
monitor_id : usize ,
) {
let mut popouts = popouts . borrow_mut ( ) ;
let Some ( handle ) = popouts . get_mut ( monitor_id ) . and_then ( | slot | slot . as_mut ( ) ) else {
return ;
} ;
handle . binding . close ( ) ;
2026-04-19 03:28:23 -03:00
if state . feed_source_preset ( monitor_id ) = = FeedSourcePreset ::Off {
handle
. picture
. set_paintable ( Option ::< & gtk ::gdk ::Paintable > ::None ) ;
handle . status_label . set_text ( " Feed disabled. " ) ;
return ;
}
2026-04-16 12:58:05 -03:00
if let Some ( binding ) = preview . install_on_picture (
monitor_id ,
super ::preview ::PreviewSurface ::Window ,
& handle . picture ,
& handle . status_label ,
) {
handle . binding = binding ;
}
2026-04-19 15:07:24 -03:00
let capture = state
. display_capture_size_choice ( monitor_id )
. unwrap_or_else ( | | state . capture_size_choice ( monitor_id ) ) ;
handle
. frame
. set_ratio ( capture . preset . display_aspect_ratio ( ) ) ;
2026-04-16 12:58:05 -03:00
}
2026-04-18 02:26:24 -03:00
#[ cfg(not(coverage)) ]
2026-04-18 03:25:20 -03:00
fn apply_preview_profiles ( preview : & super ::preview ::LauncherPreview , state : & LauncherState ) {
2026-04-18 02:26:24 -03:00
for monitor_id in 0 .. 2 {
2026-04-19 03:28:23 -03:00
let enabled = state . feed_source_preset ( monitor_id ) ! = FeedSourcePreset ::Off ;
preview . set_monitor_enabled ( monitor_id , enabled ) ;
2026-04-19 11:42:41 -03:00
let capture = state
. display_capture_size_choice ( monitor_id )
. unwrap_or_else ( | | state . capture_size_choice ( monitor_id ) ) ;
2026-04-19 03:28:23 -03:00
let source_monitor_id = state
. resolved_feed_monitor_id ( monitor_id )
. unwrap_or ( monitor_id ) ;
2026-04-18 02:26:24 -03:00
let breakout = state . breakout_size_choice ( monitor_id ) ;
preview . set_capture_profile (
monitor_id ,
2026-04-19 03:28:23 -03:00
source_monitor_id ,
2026-04-18 02:26:24 -03:00
capture . width ,
capture . height ,
capture . fps ,
capture . max_bitrate_kbit ,
) ;
preview . set_breakout_profile ( monitor_id , breakout . width , breakout . height ) ;
2026-04-18 03:25:20 -03:00
}
}
#[ cfg(not(coverage)) ]
fn sync_preview_profiles (
preview : & super ::preview ::LauncherPreview ,
widgets : & super ::ui_components ::LauncherWidgets ,
popouts : & Rc < RefCell < [ Option < super ::ui_components ::PopoutWindowHandle > ; 2 ] > > ,
state : & LauncherState ,
) {
apply_preview_profiles ( preview , state ) ;
for monitor_id in 0 .. 2 {
2026-04-19 03:28:23 -03:00
rebind_inline_preview ( preview , widgets , state , monitor_id ) ;
rebind_popout_preview ( preview , popouts , state , monitor_id ) ;
2026-04-18 02:26:24 -03:00
}
}
2026-04-15 04:11:47 -03:00
#[ cfg(not(coverage)) ]
fn disconnected_capture_note ( mode : & str ) -> & 'static str {
match mode {
" forced-on " = > " Relay disconnected. Capture is still forced on for staging. " ,
2026-04-15 04:44:06 -03:00
" forced-off " = > {
" Relay disconnected. Capture stays intentionally dark until you return to Auto or Force On. "
}
_ = > {
" Relay disconnected. The server will hold capture briefly, then let it return to standby. "
}
2026-04-15 04:11:47 -03:00
}
}
2026-04-16 12:58:05 -03:00
/// Keeps remote eye previews tied to a live session while respecting forced-off staging.
fn session_preview_active (
state : & crate ::launcher ::state ::LauncherState ,
child_running : bool ,
) -> bool {
( child_running | | state . remote_active ) & & state . capture_power . mode ! = " forced-off "
}
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 ) ;
2026-04-15 04:22:25 -03:00
let child_proc = Rc ::new ( RefCell ::new ( None ::< RelayChild > ) ) ;
2026-04-14 23:03:18 -03:00
let tests = Rc ::new ( RefCell ::new ( DeviceTestController ::new ( ) ) ) ;
2026-04-13 23:11:35 -03:00
let server_addr = Rc ::new ( server_addr ) ;
2026-04-14 18:44:40 -03:00
let focus_signal_path = Rc ::new ( launcher_focus_signal_path ( ) ) ;
2026-04-16 12:58:05 -03:00
let clipboard_control_path = Rc ::new ( launcher_clipboard_control_path ( ) ) ;
2026-04-14 20:05:26 -03:00
let input_control_path = Rc ::new ( input_control_path ( ) ) ;
let input_state_path = Rc ::new ( input_state_path ( ) ) ;
2026-04-14 18:44:40 -03:00
let _ = std ::fs ::remove_file ( focus_signal_path . as_path ( ) ) ;
2026-04-16 12:58:05 -03:00
let _ = std ::fs ::remove_file ( clipboard_control_path . as_path ( ) ) ;
2026-04-14 20:05:26 -03:00
let _ = std ::fs ::remove_file ( input_control_path . as_path ( ) ) ;
let _ = std ::fs ::remove_file ( input_state_path . as_path ( ) ) ;
2026-04-13 23:11:35 -03:00
{
let child_proc = Rc ::clone ( & child_proc ) ;
2026-04-14 20:05:26 -03:00
let focus_signal_path = Rc ::clone ( & focus_signal_path ) ;
2026-04-16 12:58:05 -03:00
let clipboard_control_path = Rc ::clone ( & clipboard_control_path ) ;
2026-04-14 20:05:26 -03:00
let input_control_path = Rc ::clone ( & input_control_path ) ;
let input_state_path = Rc ::clone ( & input_state_path ) ;
2026-04-14 23:03:18 -03:00
let tests = Rc ::clone ( & tests ) ;
2026-04-13 23:11:35 -03:00
app . connect_shutdown ( move | _ | {
2026-04-14 20:05:26 -03:00
stop_child_process ( & child_proc ) ;
2026-04-14 23:03:18 -03:00
tests . borrow_mut ( ) . stop_all ( ) ;
2026-04-14 20:05:26 -03:00
let _ = std ::fs ::remove_file ( focus_signal_path . as_path ( ) ) ;
2026-04-16 12:58:05 -03:00
let _ = std ::fs ::remove_file ( clipboard_control_path . as_path ( ) ) ;
2026-04-14 20:05:26 -03:00
let _ = std ::fs ::remove_file ( input_control_path . as_path ( ) ) ;
let _ = std ::fs ::remove_file ( input_state_path . as_path ( ) ) ;
2026-04-13 23:11:35 -03:00
} ) ;
}
{
let catalog = Rc ::clone ( & catalog ) ;
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
2026-04-14 23:03:18 -03:00
let tests = Rc ::clone ( & tests ) ;
2026-04-13 23:11:35 -03:00
let server_addr = Rc ::clone ( & server_addr ) ;
2026-04-14 18:44:40 -03:00
let focus_signal_path = Rc ::clone ( & focus_signal_path ) ;
2026-04-14 20:05:26 -03:00
let input_control_path = Rc ::clone ( & input_control_path ) ;
let input_state_path = Rc ::clone ( & input_state_path ) ;
2026-04-13 23:11:35 -03:00
app . connect_activate ( move | app | {
2026-04-16 12:58:05 -03:00
let ( display_width , display_height ) = largest_monitor_size ( ) ;
let ( physical_width , physical_height ) = largest_monitor_physical_size ( ) ;
{
let mut state = state . borrow_mut ( ) ;
state . set_breakout_display_size ( display_width , display_height ) ;
state . set_breakout_limit_size ( physical_width , physical_height ) ;
}
2026-04-14 23:03:18 -03:00
let view = build_launcher_view ( app , server_addr . as_ref ( ) , & catalog , & state . borrow ( ) ) ;
let window = view . window . clone ( ) ;
2026-04-19 04:24:27 -03:00
let ( launcher_width , launcher_height ) = launcher_default_size ( display_width , display_height ) ;
window . set_default_size ( launcher_width , launcher_height ) ;
2026-04-14 23:03:18 -03:00
let server_entry = view . server_entry . clone ( ) ;
let camera_combo = view . camera_combo . clone ( ) ;
let microphone_combo = view . microphone_combo . clone ( ) ;
let speaker_combo = view . speaker_combo . clone ( ) ;
2026-04-16 12:58:05 -03:00
let keyboard_combo = view . keyboard_combo . clone ( ) ;
let mouse_combo = view . mouse_combo . clone ( ) ;
2026-04-14 23:03:18 -03:00
let widgets = view . widgets . clone ( ) ;
let preview = view . preview . clone ( ) ;
let popouts = Rc ::clone ( & view . popouts ) ;
2026-04-16 19:19:37 -03:00
let diagnostics_popout = Rc ::clone ( & view . diagnostics_popout ) ;
2026-04-16 12:58:05 -03:00
let log_popout = Rc ::clone ( & view . log_popout ) ;
2026-04-17 04:35:41 -03:00
let shutdown_cleaned = Rc ::new ( Cell ::new ( false ) ) ;
{
let shutdown_cleaned = Rc ::clone ( & shutdown_cleaned ) ;
2026-04-17 04:53:31 -03:00
let app = app . clone ( ) ;
2026-04-17 04:35:41 -03:00
let child_proc = Rc ::clone ( & child_proc ) ;
let tests = Rc ::clone ( & tests ) ;
let preview = preview . clone ( ) ;
let widgets = widgets . clone ( ) ;
let popouts = Rc ::clone ( & popouts ) ;
let diagnostics_popout = Rc ::clone ( & diagnostics_popout ) ;
let log_popout = Rc ::clone ( & log_popout ) ;
window . connect_close_request ( move | _ | {
if ! shutdown_cleaned . replace ( true ) {
shutdown_launcher_runtime (
& child_proc ,
& tests ,
preview . as_deref ( ) ,
& widgets ,
& popouts ,
& diagnostics_popout ,
& log_popout ,
) ;
}
2026-04-17 04:53:31 -03:00
let app = app . clone ( ) ;
glib ::idle_add_local_once ( move | | {
app . quit ( ) ;
} ) ;
glib ::Propagation ::Stop
2026-04-17 04:35:41 -03:00
} ) ;
}
{
let shutdown_cleaned = Rc ::clone ( & shutdown_cleaned ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let tests = Rc ::clone ( & tests ) ;
let preview = preview . clone ( ) ;
let widgets = widgets . clone ( ) ;
let popouts = Rc ::clone ( & popouts ) ;
let diagnostics_popout = Rc ::clone ( & diagnostics_popout ) ;
let log_popout = Rc ::clone ( & log_popout ) ;
app . connect_shutdown ( move | _ | {
if ! shutdown_cleaned . replace ( true ) {
shutdown_launcher_runtime (
& child_proc ,
& tests ,
preview . as_deref ( ) ,
& widgets ,
& popouts ,
& diagnostics_popout ,
& log_popout ,
) ;
}
} ) ;
}
2026-04-14 20:05:26 -03:00
2026-04-15 01:20:51 -03:00
{
let mut tests = tests . borrow_mut ( ) ;
if let Err ( err ) = tests . bind_camera_preview (
& view . device_stage . camera_preview ,
& view . device_stage . camera_status ,
) {
widgets
. status_label
. set_text ( & format! ( " Camera preview setup failed: {err} " ) ) ;
}
if let Err ( err ) =
tests . set_camera_selection ( state . borrow ( ) . devices . camera . as_deref ( ) )
{
widgets
. status_label
. set_text ( & format! ( " Camera staging setup failed: {err} " ) ) ;
}
}
2026-04-14 20:05:26 -03:00
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_proc . borrow ( ) . is_some ( ) ) ;
2026-04-14 23:03:18 -03:00
refresh_test_buttons ( & widgets , & mut tests . borrow_mut ( ) ) ;
let ( power_tx , power_rx ) = std ::sync ::mpsc ::channel ::< PowerMessage > ( ) ;
let power_request_in_flight = Rc ::new ( Cell ::new ( false ) ) ;
2026-04-15 04:44:06 -03:00
let ( relay_tx , relay_rx ) = std ::sync ::mpsc ::channel ::< RelayMessage > ( ) ;
let relay_request_in_flight = Rc ::new ( Cell ::new ( false ) ) ;
2026-04-16 12:58:05 -03:00
let ( caps_tx , caps_rx ) = std ::sync ::mpsc ::channel ::< CapsMessage > ( ) ;
2026-04-16 21:18:34 -03:00
let caps_request_in_flight = Rc ::new ( Cell ::new ( false ) ) ;
let diagnostics_network = Rc ::new ( RefCell ::new ( NetworkTelemetry ::default ( ) ) ) ;
2026-04-17 06:14:54 -03:00
let diagnostics_process = Rc ::new ( RefCell ::new ( ProcessCpuSampler ::new ( ) ) ) ;
2026-04-20 01:41:57 -03:00
let next_power_probe =
Rc ::new ( Cell ::new ( Instant ::now ( ) + Duration ::from_millis ( 500 ) ) ) ;
2026-04-16 21:18:34 -03:00
let next_diagnostics_probe =
Rc ::new ( Cell ::new ( Instant ::now ( ) + Duration ::from_millis ( 250 ) ) ) ;
let next_diagnostics_sample =
Rc ::new ( Cell ::new ( Instant ::now ( ) + Duration ::from_secs ( 1 ) ) ) ;
2026-04-16 12:58:05 -03:00
let ( clipboard_tx , clipboard_rx ) = std ::sync ::mpsc ::channel ::< ClipboardMessage > ( ) ;
let ( log_tx , log_rx ) = std ::sync ::mpsc ::channel ::< String > ( ) ;
if let Some ( preview ) = preview . as_ref ( ) {
preview . set_log_sink ( log_tx . clone ( ) ) ;
2026-04-18 02:26:24 -03:00
sync_preview_profiles ( preview , & widgets , & popouts , & state . borrow ( ) ) ;
2026-04-16 12:58:05 -03:00
}
2026-04-14 18:44:40 -03:00
{
2026-04-14 23:03:18 -03:00
let state = Rc ::clone ( & state ) ;
2026-04-14 20:05:26 -03:00
let widgets = widgets . clone ( ) ;
2026-04-14 23:03:18 -03:00
let child_proc = Rc ::clone ( & child_proc ) ;
2026-04-15 01:20:51 -03:00
let tests = Rc ::clone ( & tests ) ;
2026-04-14 23:03:18 -03:00
let camera_combo = camera_combo . clone ( ) ;
let camera_combo_read = camera_combo . clone ( ) ;
camera_combo . connect_changed ( move | _ | {
2026-04-15 01:20:51 -03:00
let selected = selected_combo_value ( & camera_combo_read ) ;
2026-04-15 01:57:14 -03:00
let preview_was_running =
tests . borrow_mut ( ) . is_running ( DeviceTestKind ::Camera ) ;
2026-04-15 01:20:51 -03:00
state . borrow_mut ( ) . select_camera ( selected . clone ( ) ) ;
if let Err ( err ) = tests . borrow_mut ( ) . set_camera_selection ( selected . as_deref ( ) ) {
widgets
. status_label
. set_text ( & format! ( " Camera preview update failed: {err} " ) ) ;
2026-04-15 01:57:14 -03:00
} else if preview_was_running {
widgets . status_label . set_text ( & format! (
" Local camera preview switched to {}. " ,
selected . as_deref ( ) . unwrap_or ( " auto " )
) ) ;
2026-04-15 01:20:51 -03:00
}
2026-04-14 23:03:18 -03:00
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_proc . borrow ( ) . is_some ( ) ) ;
2026-04-15 01:20:51 -03:00
refresh_test_buttons ( & widgets , & mut tests . borrow_mut ( ) ) ;
2026-04-14 23:03:18 -03:00
} ) ;
}
2026-04-16 12:58:05 -03:00
{
let state = Rc ::clone ( & state ) ;
let widgets = widgets . clone ( ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let keyboard_combo = keyboard_combo . clone ( ) ;
let keyboard_combo_read = keyboard_combo . clone ( ) ;
keyboard_combo . connect_changed ( move | _ | {
let selected = selected_combo_value ( & keyboard_combo_read ) ;
state . borrow_mut ( ) . select_keyboard ( selected . clone ( ) ) ;
let message = match selected . as_deref ( ) {
Some ( path ) = > {
format! ( " The next relay launch will listen only to keyboard {path} . " )
}
None = > " The next relay launch will listen to all keyboards. " . to_string ( ) ,
} ;
widgets . status_label . set_text ( & message ) ;
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_proc . borrow ( ) . is_some ( ) ) ;
} ) ;
}
{
let state = Rc ::clone ( & state ) ;
let widgets = widgets . clone ( ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let mouse_combo = mouse_combo . clone ( ) ;
let mouse_combo_read = mouse_combo . clone ( ) ;
mouse_combo . connect_changed ( move | _ | {
let selected = selected_combo_value ( & mouse_combo_read ) ;
state . borrow_mut ( ) . select_mouse ( selected . clone ( ) ) ;
let message = match selected . as_deref ( ) {
Some ( path ) = > {
format! ( " The next relay launch will listen only to pointer {path} . " )
}
None = > {
" The next relay launch will listen to all pointer devices. "
. to_string ( )
}
} ;
widgets . status_label . set_text ( & message ) ;
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_proc . borrow ( ) . is_some ( ) ) ;
} ) ;
}
2026-04-15 04:11:47 -03:00
if let Some ( preview ) = preview . as_ref ( ) {
preview . set_session_active ( false ) ;
}
request_capture_power_refresh (
power_tx . clone ( ) ,
selected_server_addr ( & server_entry , server_addr . as_ref ( ) ) ,
Duration ::ZERO ,
) ;
2026-04-16 21:18:34 -03:00
caps_request_in_flight . set ( true ) ;
2026-04-16 12:58:05 -03:00
request_handshake_caps (
caps_tx . clone ( ) ,
selected_server_addr ( & server_entry , server_addr . as_ref ( ) ) ,
Duration ::ZERO ,
) ;
{
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let widgets = widgets . clone ( ) ;
let server_entry = server_entry . clone ( ) ;
let server_entry_read = server_entry . clone ( ) ;
let server_addr_fallback = Rc ::clone ( & server_addr ) ;
let preview = preview . clone ( ) ;
let power_tx = power_tx . clone ( ) ;
let caps_tx = caps_tx . clone ( ) ;
2026-04-16 21:18:34 -03:00
let caps_request_in_flight = Rc ::clone ( & caps_request_in_flight ) ;
2026-04-16 12:58:05 -03:00
server_entry . connect_changed ( move | _ | {
let server_addr =
selected_server_addr ( & server_entry_read , server_addr_fallback . as_ref ( ) ) ;
2026-04-16 15:59:42 -03:00
{
let mut state = state . borrow_mut ( ) ;
state . set_server_available ( false ) ;
state . set_server_version ( None ) ;
}
2026-04-16 12:58:05 -03:00
if let Some ( preview ) = preview . as_ref ( ) {
preview . set_server_addr ( server_addr . clone ( ) ) ;
}
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_proc . borrow ( ) . is_some ( ) ) ;
request_capture_power_refresh (
power_tx . clone ( ) ,
server_addr . clone ( ) ,
Duration ::from_millis ( 150 ) ,
) ;
2026-04-16 21:18:34 -03:00
caps_request_in_flight . set ( true ) ;
2026-04-16 12:58:05 -03:00
request_handshake_caps ( caps_tx . clone ( ) , server_addr , Duration ::from_millis ( 150 ) ) ;
} ) ;
}
2026-04-19 03:28:23 -03:00
for monitor_id in 0 .. 2 {
let state = Rc ::clone ( & state ) ;
let widgets = widgets . clone ( ) ;
let popouts = Rc ::clone ( & popouts ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let preview = preview . clone ( ) ;
let feed_source_combo = widgets . display_panes [ monitor_id ] . feed_source_combo . clone ( ) ;
feed_source_combo . connect_changed ( move | combo | {
let Some ( active_id ) = combo . active_id ( ) else {
return ;
} ;
let Some ( preset ) = FeedSourcePreset ::from_id ( active_id . as_str ( ) ) else {
return ;
} ;
if state . borrow ( ) . feed_source_preset ( monitor_id ) = = preset {
return ;
}
{
let mut state = state . borrow_mut ( ) ;
state . set_feed_source_preset ( monitor_id , preset ) ;
}
if let Some ( preview ) = preview . as_ref ( ) {
sync_preview_profiles ( preview , & widgets , & popouts , & state . borrow ( ) ) ;
}
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_proc . borrow ( ) . is_some ( ) ) ;
} ) ;
}
2026-04-16 12:58:05 -03:00
for monitor_id in 0 .. 2 {
let state = Rc ::clone ( & state ) ;
let widgets = widgets . clone ( ) ;
let popouts = Rc ::clone ( & popouts ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let preview = preview . clone ( ) ;
2026-04-17 01:09:33 -03:00
let resolution_combo =
widgets . display_panes [ monitor_id ] . capture_resolution_combo . clone ( ) ;
resolution_combo . connect_changed ( move | combo | {
2026-04-16 12:58:05 -03:00
let Some ( active_id ) = combo . active_id ( ) else {
return ;
} ;
let Some ( preset ) = CaptureSizePreset ::from_id ( active_id . as_str ( ) ) else {
return ;
} ;
2026-04-19 11:42:41 -03:00
if state . borrow ( ) . feed_source_preset ( monitor_id ) ! = FeedSourcePreset ::ThisEye {
2026-04-19 03:28:23 -03:00
return ;
}
2026-04-16 12:58:05 -03:00
if state . borrow ( ) . capture_size_preset ( monitor_id ) = = preset {
return ;
}
{
let mut state = state . borrow_mut ( ) ;
state . set_capture_size_preset ( monitor_id , preset ) ;
}
if let Some ( preview ) = preview . as_ref ( ) {
2026-04-19 11:42:41 -03:00
let choice = state
. borrow ( )
. display_capture_size_choice ( monitor_id )
. unwrap_or_else ( | | state . borrow ( ) . capture_size_choice ( monitor_id ) ) ;
2026-04-19 03:28:23 -03:00
let source_monitor_id = state
. borrow ( )
. resolved_feed_monitor_id ( monitor_id )
. unwrap_or ( monitor_id ) ;
2026-04-17 01:09:33 -03:00
preview . set_capture_profile (
monitor_id ,
2026-04-19 03:28:23 -03:00
source_monitor_id ,
2026-04-17 01:09:33 -03:00
choice . width ,
choice . height ,
choice . fps ,
choice . max_bitrate_kbit ,
) ;
2026-04-19 03:28:23 -03:00
sync_preview_profiles ( preview , & widgets , & popouts , & state . borrow ( ) ) ;
2026-04-16 12:58:05 -03:00
}
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_proc . borrow ( ) . is_some ( ) ) ;
} ) ;
}
for monitor_id in 0 .. 2 {
let state = Rc ::clone ( & state ) ;
let widgets = widgets . clone ( ) ;
let popouts = Rc ::clone ( & popouts ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let preview = preview . clone ( ) ;
let breakout_combo = widgets . display_panes [ monitor_id ] . breakout_combo . clone ( ) ;
breakout_combo . connect_changed ( move | combo | {
let Some ( active_id ) = combo . active_id ( ) else {
return ;
} ;
let Some ( preset ) = BreakoutSizePreset ::from_id ( active_id . as_str ( ) ) else {
return ;
} ;
if state . borrow ( ) . breakout_size_preset ( monitor_id ) = = preset {
return ;
}
{
let mut state = state . borrow_mut ( ) ;
state . set_breakout_size_preset ( monitor_id , preset ) ;
}
let size = state . borrow ( ) . breakout_size_choice ( monitor_id ) ;
if let Some ( preview ) = preview . as_ref ( ) {
preview . set_breakout_profile ( monitor_id , size . width , size . height ) ;
}
let popout_open = {
popouts
. borrow ( )
. get ( monitor_id )
. and_then ( | slot | slot . as_ref ( ) )
. is_some ( )
} ;
if popout_open {
if let Some ( preview ) = preview . as_ref ( ) {
2026-04-19 03:28:23 -03:00
rebind_popout_preview ( preview , & popouts , & state . borrow ( ) , monitor_id ) ;
2026-04-16 12:58:05 -03:00
}
if let Some ( handle ) = popouts
. borrow ( )
. get ( monitor_id )
. and_then ( | slot | slot . as_ref ( ) )
{
let display_limit = state . borrow ( ) . breakout_display_size ( ) ;
apply_popout_window_size ( handle , size , display_limit ) ;
}
}
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_proc . borrow ( ) . is_some ( ) ) ;
} ) ;
}
2026-04-15 04:11:47 -03:00
2026-04-14 23:03:18 -03:00
{
let state = Rc ::clone ( & state ) ;
let widgets = widgets . clone ( ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
2026-04-15 01:57:14 -03:00
let tests = Rc ::clone ( & tests ) ;
2026-04-14 23:03:18 -03:00
let microphone_combo = microphone_combo . clone ( ) ;
let microphone_combo_read = microphone_combo . clone ( ) ;
microphone_combo . connect_changed ( move | _ | {
state
. borrow_mut ( )
. select_microphone ( selected_combo_value ( & microphone_combo_read ) ) ;
2026-04-15 01:57:14 -03:00
if tests . borrow_mut ( ) . is_running ( DeviceTestKind ::Microphone ) {
widgets . status_label . set_text (
" Microphone selection changed. Restart Monitor Mic to audition the new input. " ,
) ;
}
2026-04-14 23:03:18 -03:00
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_proc . borrow ( ) . is_some ( ) ) ;
2026-04-15 01:57:14 -03:00
refresh_test_buttons ( & widgets , & mut tests . borrow_mut ( ) ) ;
2026-04-14 23:03:18 -03:00
} ) ;
}
{
2026-04-14 18:44:40 -03:00
let state = Rc ::clone ( & state ) ;
2026-04-14 23:03:18 -03:00
let widgets = widgets . clone ( ) ;
2026-04-14 20:05:26 -03:00
let child_proc = Rc ::clone ( & child_proc ) ;
2026-04-15 01:57:14 -03:00
let tests = Rc ::clone ( & tests ) ;
2026-04-14 23:03:18 -03:00
let speaker_combo = speaker_combo . clone ( ) ;
let speaker_combo_read = speaker_combo . clone ( ) ;
speaker_combo . connect_changed ( move | _ | {
state
. borrow_mut ( )
. select_speaker ( selected_combo_value ( & speaker_combo_read ) ) ;
2026-04-15 01:57:14 -03:00
let speaker_running = tests . borrow_mut ( ) . is_running ( DeviceTestKind ::Speaker ) ;
let microphone_running =
tests . borrow_mut ( ) . is_running ( DeviceTestKind ::Microphone ) ;
if speaker_running | | microphone_running {
widgets . status_label . set_text (
" Speaker selection changed. Restart the local audio tests to hear the new output. " ,
) ;
}
2026-04-14 20:05:26 -03:00
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_proc . borrow ( ) . is_some ( ) ) ;
2026-04-15 01:57:14 -03:00
refresh_test_buttons ( & widgets , & mut tests . borrow_mut ( ) ) ;
2026-04-14 18:44:40 -03:00
} ) ;
2026-04-14 14:38:03 -03:00
}
2026-04-20 11:11:51 -03:00
{
let state = Rc ::clone ( & state ) ;
let widgets = widgets . clone ( ) ;
let widgets_handle = widgets . clone ( ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let tests = Rc ::clone ( & tests ) ;
let camera_combo = camera_combo . clone ( ) ;
let microphone_combo = microphone_combo . clone ( ) ;
let speaker_combo = speaker_combo . clone ( ) ;
let keyboard_combo = keyboard_combo . clone ( ) ;
let mouse_combo = mouse_combo . clone ( ) ;
widgets . device_refresh_button . connect_clicked ( move | _ | {
let catalog = DeviceCatalog ::discover ( ) ;
let (
selected_camera ,
selected_microphone ,
selected_speaker ,
selected_keyboard ,
selected_mouse ,
) = {
let state = state . borrow ( ) ;
(
retained_stage_selection (
state . devices . camera . as_deref ( ) ,
& catalog . cameras ,
) ,
retained_stage_selection (
state . devices . microphone . as_deref ( ) ,
& catalog . microphones ,
) ,
retained_stage_selection (
state . devices . speaker . as_deref ( ) ,
& catalog . speakers ,
) ,
retained_input_selection (
state . devices . keyboard . as_deref ( ) ,
& catalog . keyboards ,
) ,
retained_input_selection ( state . devices . mouse . as_deref ( ) , & catalog . mice ) ,
)
} ;
{
let mut state = state . borrow_mut ( ) ;
state . select_camera ( selected_camera ) ;
state . select_microphone ( selected_microphone ) ;
state . select_speaker ( selected_speaker ) ;
state . select_keyboard ( selected_keyboard ) ;
state . select_mouse ( selected_mouse ) ;
}
let state_snapshot = state . borrow ( ) . clone ( ) ;
sync_stage_device_combo (
& camera_combo ,
& catalog . cameras ,
state_snapshot . devices . camera . as_deref ( ) ,
) ;
sync_stage_device_combo (
& microphone_combo ,
& catalog . microphones ,
state_snapshot . devices . microphone . as_deref ( ) ,
) ;
sync_stage_device_combo (
& speaker_combo ,
& catalog . speakers ,
state_snapshot . devices . speaker . as_deref ( ) ,
) ;
sync_input_device_combo (
& keyboard_combo ,
& catalog . keyboards ,
state_snapshot . devices . keyboard . as_deref ( ) ,
" all keyboards " ,
) ;
sync_input_device_combo (
& mouse_combo ,
& catalog . mice ,
state_snapshot . devices . mouse . as_deref ( ) ,
" all mice " ,
) ;
if let Err ( err ) = tests
. borrow_mut ( )
. set_camera_selection ( state_snapshot . devices . camera . as_deref ( ) )
{
widgets_handle
. status_label
. set_text ( & format! ( " Device refresh succeeded, but the webcam test could not switch cleanly: {err} " ) ) ;
} else {
2026-04-20 13:59:34 -03:00
let message = if usb_audio_kernel_support_missing ( ) {
" Device staging refreshed. USB audio devices may still stay invisible until the host boots a kernel with snd_usb_audio available; reconnect the relay if you want the live session to use a new webcam, mic, or speaker. "
} else {
" Device staging refreshed. Newly attached devices are ready for local tests; reconnect the relay if you want the live session to use a new webcam, mic, or speaker. "
} ;
widgets_handle . status_label . set_text ( message ) ;
2026-04-20 11:11:51 -03:00
}
refresh_launcher_ui (
& widgets_handle ,
& state . borrow ( ) ,
child_proc . borrow ( ) . is_some ( ) ,
) ;
refresh_test_buttons ( & widgets_handle , & mut tests . borrow_mut ( ) ) ;
} ) ;
}
2026-04-13 23:11:35 -03:00
{
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
2026-04-14 20:05:26 -03:00
let widgets = widgets . clone ( ) ;
let server_entry = server_entry . clone ( ) ;
2026-04-13 23:11:35 -03:00
let camera_combo = camera_combo . clone ( ) ;
let microphone_combo = microphone_combo . clone ( ) ;
let speaker_combo = speaker_combo . clone ( ) ;
2026-04-14 20:05:26 -03:00
let input_control_path = Rc ::clone ( & input_control_path ) ;
let input_state_path = Rc ::clone ( & input_state_path ) ;
let server_addr_fallback = Rc ::clone ( & server_addr ) ;
2026-04-15 04:11:47 -03:00
let preview = preview . clone ( ) ;
let power_tx = power_tx . clone ( ) ;
2026-04-15 04:44:06 -03:00
let relay_tx = relay_tx . clone ( ) ;
let relay_request_in_flight = Rc ::clone ( & relay_request_in_flight ) ;
2026-04-16 12:58:05 -03:00
let popouts = Rc ::clone ( & popouts ) ;
let window = window . clone ( ) ;
2026-04-14 23:03:18 -03:00
let start_button = widgets . start_button . clone ( ) ;
let widgets_handle = widgets . clone ( ) ;
2026-04-13 23:11:35 -03:00
start_button . connect_clicked ( move | _ | {
2026-04-15 04:11:47 -03:00
let server_addr =
selected_server_addr ( & server_entry , server_addr_fallback . as_ref ( ) ) ;
2026-04-15 04:44:06 -03:00
if relay_request_in_flight . get ( ) {
return ;
}
2026-04-14 20:05:26 -03:00
if child_proc . borrow ( ) . is_some ( ) {
2026-04-15 04:11:47 -03:00
stop_child_process ( & child_proc ) ;
let power_mode = {
let mut state = state . borrow_mut ( ) ;
let _ = state . stop_remote ( ) ;
state . capture_power . mode . clone ( )
} ;
2026-04-16 12:58:05 -03:00
dock_all_displays_to_preview (
& state ,
& child_proc ,
& popouts ,
& widgets_handle ,
) ;
window . present ( ) ;
2026-04-15 04:11:47 -03:00
if let Some ( preview ) = preview . as_ref ( ) {
preview . set_server_addr ( server_addr . clone ( ) ) ;
preview . set_session_active ( false ) ;
}
2026-04-16 12:58:05 -03:00
if power_mode ! = " auto " {
widgets_handle . status_label . set_text (
" Relay disconnected. Returning capture to automatic mode so it can fall back after the disconnect grace. " ,
) ;
request_capture_power_command (
power_tx . clone ( ) ,
server_addr . clone ( ) ,
CapturePowerCommand ::Auto ,
) ;
} else {
widgets_handle
. status_label
. set_text ( disconnected_capture_note ( & power_mode ) ) ;
}
2026-04-15 04:11:47 -03:00
request_capture_power_refresh (
power_tx . clone ( ) ,
server_addr . clone ( ) ,
Duration ::from_millis ( 250 ) ,
) ;
request_capture_power_refresh (
power_tx . clone ( ) ,
server_addr ,
Duration ::from_secs ( 31 ) ,
) ;
refresh_launcher_ui ( & widgets_handle , & state . borrow ( ) , false ) ;
2026-04-14 20:05:26 -03:00
return ;
}
2026-04-13 23:11:35 -03:00
{
let mut state = state . borrow_mut ( ) ;
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-16 12:58:05 -03:00
state . select_keyboard ( selected_combo_value ( & keyboard_combo ) ) ;
state . select_mouse ( selected_combo_value ( & mouse_combo ) ) ;
2026-04-13 23:11:35 -03:00
}
2026-04-14 20:05:26 -03:00
let _ = std ::fs ::remove_file ( input_control_path . as_path ( ) ) ;
let _ = std ::fs ::remove_file ( input_state_path . as_path ( ) ) ;
let launch_state = state . borrow ( ) . clone ( ) ;
2026-04-15 04:44:06 -03:00
let input_toggle_key = launch_state . swap_key . clone ( ) ;
let input_control_path = input_control_path . as_ref ( ) . clone ( ) ;
let input_state_path = input_state_path . as_ref ( ) . clone ( ) ;
relay_request_in_flight . set ( true ) ;
widgets_handle . status_label . set_text ( & format! (
" Connecting relay with {} as the swap key... " ,
toggle_key_label ( & input_toggle_key )
) ) ;
2026-04-14 23:03:18 -03:00
refresh_launcher_ui (
& widgets_handle ,
& state . borrow ( ) ,
child_proc . borrow ( ) . is_some ( ) ,
) ;
2026-04-15 04:44:06 -03:00
let relay_tx = relay_tx . clone ( ) ;
std ::thread ::spawn ( move | | {
let result = spawn_client_process (
& server_addr ,
& launch_state ,
& input_toggle_key ,
input_control_path . as_path ( ) ,
input_state_path . as_path ( ) ,
)
. map_err ( | err | err . to_string ( ) ) ;
let _ = relay_tx . send ( RelayMessage ::Spawned ( result ) ) ;
} ) ;
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 ) ;
2026-04-14 20:05:26 -03:00
let widgets = widgets . clone ( ) ;
let input_control_path = Rc ::clone ( & input_control_path ) ;
2026-04-16 12:58:05 -03:00
let popouts = Rc ::clone ( & popouts ) ;
let window = window . clone ( ) ;
2026-04-14 23:03:18 -03:00
let input_toggle_button = widgets . input_toggle_button . clone ( ) ;
let widgets_handle = widgets . clone ( ) ;
2026-04-14 20:05:26 -03:00
input_toggle_button . connect_clicked ( move | _ | {
let next = next_input_routing ( state . borrow ( ) . routing ) ;
let child_running = child_proc . borrow ( ) . is_some ( ) ;
if child_running {
if let Err ( err ) =
write_input_routing_request ( input_control_path . as_path ( ) , next )
{
2026-04-14 23:03:18 -03:00
widgets_handle
. status_label
. set_text ( & format! ( " Could not update live input target: {err} " ) ) ;
refresh_launcher_ui ( & widgets_handle , & state . borrow ( ) , true ) ;
2026-04-14 20:05:26 -03:00
return ;
2026-04-14 04:02:39 -03:00
}
2026-04-14 23:03:18 -03:00
widgets_handle . status_label . set_text ( & format! (
" Input routing switched toward {}. " ,
2026-04-14 20:05:26 -03:00
routing_name ( next )
) ) ;
2026-04-14 04:02:39 -03:00
} else {
2026-04-14 23:03:18 -03:00
widgets_handle . status_label . set_text ( & format! (
" Relay will start with {} input ownership. " ,
2026-04-14 20:05:26 -03:00
routing_name ( next )
) ) ;
2026-04-14 04:02:39 -03:00
}
2026-04-14 20:05:26 -03:00
state . borrow_mut ( ) . set_routing ( next ) ;
2026-04-14 23:03:18 -03:00
refresh_launcher_ui ( & widgets_handle , & state . borrow ( ) , child_running ) ;
2026-04-16 12:58:05 -03:00
if matches! ( next , InputRouting ::Remote ) {
present_popout_windows ( & popouts ) ;
} else {
window . present ( ) ;
}
2026-04-14 04:02:39 -03:00
} ) ;
}
2026-04-15 04:44:06 -03:00
{
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let widgets = widgets . clone ( ) ;
let swap_key_button = widgets . swap_key_button . clone ( ) ;
swap_key_button . connect_clicked ( move | _ | {
2026-04-16 12:58:05 -03:00
let token = state . borrow_mut ( ) . begin_swap_key_binding ( ) ;
2026-04-15 04:44:06 -03:00
widgets
. status_label
2026-04-16 12:58:05 -03:00
. set_text ( " Press a single key within 3 seconds to make it the swap shortcut. " ) ;
2026-04-15 04:44:06 -03:00
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_proc . borrow ( ) . is_some ( ) ) ;
2026-04-16 12:58:05 -03:00
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let widgets = widgets . clone ( ) ;
glib ::timeout_add_local_once ( Duration ::from_secs ( 3 ) , move | | {
if state . borrow_mut ( ) . cancel_swap_key_binding ( token ) {
widgets . status_label . set_text (
" Swap-key capture timed out. The previous shortcut is still in place. " ,
) ;
refresh_launcher_ui (
& widgets ,
& state . borrow ( ) ,
child_proc . borrow ( ) . is_some ( ) ,
) ;
}
} ) ;
2026-04-15 04:44:06 -03:00
} ) ;
}
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-14 20:05:26 -03:00
let widgets = widgets . clone ( ) ;
2026-04-14 13:09:25 -03:00
let server_entry = server_entry . clone ( ) ;
2026-04-14 20:05:26 -03:00
let server_addr_fallback = Rc ::clone ( & server_addr ) ;
2026-04-16 12:58:05 -03:00
let clipboard_tx = clipboard_tx . clone ( ) ;
2026-04-14 23:03:18 -03:00
widgets . clipboard_button . connect_clicked ( move | _ | {
2026-04-14 13:09:25 -03:00
if child_proc . borrow ( ) . is_none ( ) {
2026-04-14 23:03:18 -03:00
widgets
. status_label
. set_text ( " Start the relay before sending clipboard text. " ) ;
2026-04-14 13:09:25 -03:00
return ;
}
2026-04-14 20:05:26 -03:00
let server_addr =
selected_server_addr ( & server_entry , server_addr_fallback . as_ref ( ) ) ;
2026-04-16 12:58:05 -03:00
let Some ( display ) = gtk ::gdk ::Display ::default ( ) else {
widgets
. status_label
. set_text ( " No desktop clipboard is available in this session. " ) ;
return ;
} ;
2026-04-14 23:03:18 -03:00
widgets
. status_label
2026-04-16 12:58:05 -03:00
. set_text ( " Reading the local clipboard and packing a remote paste spell... " ) ;
let clipboard = display . clipboard ( ) ;
let clipboard_tx = clipboard_tx . clone ( ) ;
clipboard . read_text_async ( None ::< & gtk ::gio ::Cancellable > , move | result | {
match result {
Ok ( Some ( text ) ) = > {
let text = text . trim_end_matches ( [ '\r' , '\n' ] ) . to_string ( ) ;
if text . is_empty ( ) {
let _ = clipboard_tx . send ( ClipboardMessage ::Finished ( Err (
" clipboard is empty " . to_string ( ) ,
) ) ) ;
return ;
}
let clipboard_tx = clipboard_tx . clone ( ) ;
std ::thread ::spawn ( move | | {
let result = send_clipboard_text_to_remote ( & server_addr , & text )
. map_err ( | err | err . to_string ( ) ) ;
let _ = clipboard_tx
. send ( ClipboardMessage ::Finished ( result ) ) ;
} ) ;
2026-04-14 18:44:40 -03:00
}
2026-04-16 12:58:05 -03:00
Ok ( None ) = > {
let _ = clipboard_tx . send ( ClipboardMessage ::Finished ( Err (
" clipboard is empty " . to_string ( ) ,
) ) ) ;
2026-04-14 18:44:40 -03:00
}
2026-04-16 12:58:05 -03:00
Err ( err ) = > {
let _ = clipboard_tx . send ( ClipboardMessage ::Finished ( Err (
format! ( " clipboard read failed: {err} " ) ,
) ) ) ;
2026-04-14 18:44:40 -03:00
}
}
} ) ;
2026-04-13 23:11:35 -03:00
} ) ;
}
2026-04-14 23:03:18 -03:00
{
let widgets = widgets . clone ( ) ;
widgets . probe_button . connect_clicked ( move | _ | {
if let Some ( display ) = gtk ::gdk ::Display ::default ( ) {
let clipboard = display . clipboard ( ) ;
clipboard . set_text ( quality_probe_command ( ) ) ;
widgets
. status_label
. set_text ( " Quality probe command copied to the local clipboard. " ) ;
} else {
widgets
. status_label
. set_text ( " No desktop clipboard is available in this session. " ) ;
}
} ) ;
}
2026-04-20 08:38:26 -03:00
{
let widgets = widgets . clone ( ) ;
let server_entry = server_entry . clone ( ) ;
let server_addr_fallback = Rc ::clone ( & server_addr ) ;
let widgets_for_click = widgets . clone ( ) ;
widgets . usb_recover_button . connect_clicked ( move | _ | {
let server_addr =
selected_server_addr ( & server_entry , server_addr_fallback . as_ref ( ) ) ;
widgets_for_click . status_label . set_text (
" Requesting a forced USB gadget re-enumeration on the relay host... " ,
) ;
let ( tx , rx ) = std ::sync ::mpsc ::channel ( ) ;
std ::thread ::spawn ( move | | {
let result = reset_usb_gadget ( & server_addr ) . map_err ( | err | err . to_string ( ) ) ;
let _ = tx . send ( result ) ;
} ) ;
let widgets = widgets_for_click . clone ( ) ;
glib ::timeout_add_local ( Duration ::from_millis ( 100 ) , move | | {
match rx . try_recv ( ) {
Ok ( Ok ( ( ) ) ) = > {
widgets . status_label . set_text (
" USB gadget recovery requested. Give the host a few seconds to re-enumerate keyboard, mouse, webcam, and audio. " ,
) ;
glib ::ControlFlow ::Break
}
Ok ( Err ( err ) ) = > {
widgets
. status_label
. set_text ( & format! ( " USB gadget recovery failed: {err} " ) ) ;
glib ::ControlFlow ::Break
}
Err ( std ::sync ::mpsc ::TryRecvError ::Empty ) = > glib ::ControlFlow ::Continue ,
Err ( std ::sync ::mpsc ::TryRecvError ::Disconnected ) = > {
widgets . status_label . set_text (
" USB gadget recovery ended unexpectedly before the relay answered. " ,
) ;
glib ::ControlFlow ::Break
}
}
} ) ;
} ) ;
}
2026-04-16 12:58:05 -03:00
{
let widgets = widgets . clone ( ) ;
2026-04-16 19:19:37 -03:00
widgets . diagnostics_copy_button . connect_clicked ( move | _ | {
2026-04-20 12:12:29 -03:00
if let Err ( err ) = copy_plain_text ( & widgets . diagnostics_rendered_text . borrow ( ) ) {
2026-04-16 19:19:37 -03:00
widgets
. status_label
. set_text ( & format! ( " Could not copy the diagnostics report: {err} " ) ) ;
} else {
widgets
. status_label
. set_text ( " Diagnostics report copied to the local clipboard. " ) ;
}
} ) ;
}
{
let app = app . clone ( ) ;
let widgets = widgets . clone ( ) ;
let diagnostics_popout = Rc ::clone ( & diagnostics_popout ) ;
widgets . diagnostics_popout_button . connect_clicked ( move | _ | {
2026-04-20 08:57:13 -03:00
open_diagnostics_popout (
& app ,
& diagnostics_popout ,
2026-04-20 12:12:29 -03:00
& widgets . diagnostics_popout_label ,
2026-04-20 08:57:13 -03:00
& widgets . diagnostics_popout_scroll ,
2026-04-20 12:12:29 -03:00
& widgets . diagnostics_rendered_text ,
2026-04-20 08:57:13 -03:00
) ;
2026-04-16 19:19:37 -03:00
widgets
. status_label
. set_text ( " Diagnostics report moved into its own window. " ) ;
} ) ;
}
{
let widgets = widgets . clone ( ) ;
2026-04-16 12:58:05 -03:00
widgets . console_copy_button . connect_clicked ( move | _ | {
if let Err ( err ) = copy_session_log ( & widgets . session_log_buffer ) {
widgets
. status_label
. set_text ( & format! ( " Could not copy the session log: {err} " ) ) ;
} else {
widgets
. status_label
. set_text ( " Session log copied to the local clipboard. " ) ;
}
} ) ;
}
{
let app = app . clone ( ) ;
let widgets = widgets . clone ( ) ;
let log_popout = Rc ::clone ( & log_popout ) ;
widgets . console_popout_button . connect_clicked ( move | _ | {
open_session_log_popout ( & app , & log_popout , & widgets . session_log_buffer ) ;
widgets
. status_label
. set_text ( " Session log moved into its own window. " ) ;
} ) ;
}
2026-04-14 23:03:18 -03:00
{
let widgets = widgets . clone ( ) ;
let tests = Rc ::clone ( & tests ) ;
let camera_combo = camera_combo . clone ( ) ;
let camera_test_button = widgets . camera_test_button . clone ( ) ;
let widgets_handle = widgets . clone ( ) ;
camera_test_button . connect_clicked ( move | _ | {
2026-04-15 01:20:51 -03:00
let selected = selected_combo_value ( & camera_combo ) ;
let result = {
let mut tests = tests . borrow_mut ( ) ;
let _ = tests . set_camera_selection ( selected . as_deref ( ) ) ;
tests . toggle_camera ( )
} ;
2026-04-14 23:03:18 -03:00
update_test_action_result (
& widgets_handle ,
& mut tests . borrow_mut ( ) ,
result ,
2026-04-15 01:57:14 -03:00
" Camera preview started inside the launcher. This stays local until you start the relay. " ,
" Camera preview stopped. The selected camera stays staged for the next launch. " ,
2026-04-14 23:03:18 -03:00
) ;
} ) ;
}
{
let widgets = widgets . clone ( ) ;
let tests = Rc ::clone ( & tests ) ;
let microphone_combo = microphone_combo . clone ( ) ;
let speaker_combo = speaker_combo . clone ( ) ;
let microphone_test_button = widgets . microphone_test_button . clone ( ) ;
let widgets_handle = widgets . clone ( ) ;
microphone_test_button . connect_clicked ( move | _ | {
let mic = selected_combo_value ( & microphone_combo ) ;
let sink = selected_combo_value ( & speaker_combo ) ;
let result = tests
. borrow_mut ( )
. toggle_microphone ( mic . as_deref ( ) , sink . as_deref ( ) ) ;
update_test_action_result (
& widgets_handle ,
& mut tests . borrow_mut ( ) ,
result ,
2026-04-15 01:57:14 -03:00
" Microphone monitor started locally through the selected speaker. " ,
2026-04-14 23:03:18 -03:00
" Microphone monitor stopped. " ,
) ;
} ) ;
}
2026-04-16 12:58:05 -03:00
{
let widgets = widgets . clone ( ) ;
let tests = Rc ::clone ( & tests ) ;
let speaker_combo = speaker_combo . clone ( ) ;
let microphone_replay_button = widgets . microphone_replay_button . clone ( ) ;
let widgets_handle = widgets . clone ( ) ;
microphone_replay_button . connect_clicked ( move | _ | {
let result = tests
. borrow_mut ( )
. toggle_microphone_replay ( selected_combo_value ( & speaker_combo ) . as_deref ( ) ) ;
update_test_action_result (
& widgets_handle ,
& mut tests . borrow_mut ( ) ,
result ,
" Replaying the latest local mic capture through the selected speaker. " ,
" Mic replay stopped. " ,
) ;
} ) ;
}
2026-04-14 23:03:18 -03:00
{
let widgets = widgets . clone ( ) ;
let tests = Rc ::clone ( & tests ) ;
let speaker_combo = speaker_combo . clone ( ) ;
let speaker_test_button = widgets . speaker_test_button . clone ( ) ;
let widgets_handle = widgets . clone ( ) ;
speaker_test_button . connect_clicked ( move | _ | {
let result = tests
. borrow_mut ( )
. toggle_speaker ( selected_combo_value ( & speaker_combo ) . as_deref ( ) ) ;
update_test_action_result (
& widgets_handle ,
& mut tests . borrow_mut ( ) ,
result ,
2026-04-15 01:57:14 -03:00
" Speaker tone started locally. " ,
2026-04-14 23:03:18 -03:00
" Speaker test tone stopped. " ,
) ;
} ) ;
}
{
let widgets = widgets . clone ( ) ;
let server_entry = server_entry . clone ( ) ;
let server_addr_fallback = Rc ::clone ( & server_addr ) ;
let power_tx = power_tx . clone ( ) ;
let power_request_in_flight = Rc ::clone ( & power_request_in_flight ) ;
2026-04-15 01:20:51 -03:00
let power_auto_button = widgets . power_auto_button . clone ( ) ;
let widgets_handle = widgets . clone ( ) ;
power_auto_button . connect_clicked ( move | _ | {
2026-04-14 23:03:18 -03:00
if power_request_in_flight . replace ( true ) {
return ;
}
let server_addr =
selected_server_addr ( & server_entry , server_addr_fallback . as_ref ( ) ) ;
2026-04-15 01:20:51 -03:00
widgets_handle
. status_label
. set_text ( " Returning capture feeds to automatic mode... " ) ;
2026-04-16 12:58:05 -03:00
request_capture_power_command (
power_tx . clone ( ) ,
server_addr ,
CapturePowerCommand ::Auto ,
) ;
2026-04-15 01:20:51 -03:00
} ) ;
}
{
let server_entry = server_entry . clone ( ) ;
let server_addr_fallback = Rc ::clone ( & server_addr ) ;
let power_tx = power_tx . clone ( ) ;
let power_request_in_flight = Rc ::clone ( & power_request_in_flight ) ;
let power_on_button = widgets . power_on_button . clone ( ) ;
let widgets_handle = widgets . clone ( ) ;
power_on_button . connect_clicked ( move | _ | {
if power_request_in_flight . replace ( true ) {
return ;
}
let server_addr =
selected_server_addr ( & server_entry , server_addr_fallback . as_ref ( ) ) ;
widgets_handle
. status_label
. set_text ( " Forcing capture feeds on for staging... " ) ;
2026-04-16 12:58:05 -03:00
request_capture_power_command (
power_tx . clone ( ) ,
server_addr ,
CapturePowerCommand ::ForceOn ,
) ;
2026-04-15 01:20:51 -03:00
} ) ;
}
{
let server_entry = server_entry . clone ( ) ;
let server_addr_fallback = Rc ::clone ( & server_addr ) ;
let power_tx = power_tx . clone ( ) ;
let power_request_in_flight = Rc ::clone ( & power_request_in_flight ) ;
let power_off_button = widgets . power_off_button . clone ( ) ;
let widgets_handle = widgets . clone ( ) ;
power_off_button . connect_clicked ( move | _ | {
if power_request_in_flight . replace ( true ) {
return ;
}
let server_addr =
selected_server_addr ( & server_entry , server_addr_fallback . as_ref ( ) ) ;
widgets_handle
. status_label
. set_text ( " Forcing capture feeds off for staging... " ) ;
2026-04-16 12:58:05 -03:00
request_capture_power_command (
power_tx . clone ( ) ,
server_addr ,
CapturePowerCommand ::ForceOff ,
) ;
2026-04-14 23:03:18 -03:00
} ) ;
}
2026-04-14 20:05:26 -03:00
for monitor_id in 0 .. 2 {
let app = app . clone ( ) ;
let preview = preview . clone ( ) ;
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let popouts = Rc ::clone ( & popouts ) ;
let widgets = widgets . clone ( ) ;
let action_button = widgets . display_panes [ monitor_id ] . action_button . clone ( ) ;
action_button . connect_clicked ( move | _ | {
let Some ( preview ) = preview . as_ref ( ) else {
widgets
. status_label
2026-04-14 23:03:18 -03:00
. set_text ( " Preview is unavailable for breakout windows. " ) ;
2026-04-14 20:05:26 -03:00
return ;
} ;
2026-04-16 12:58:05 -03:00
let surface = {
let state = state . borrow ( ) ;
state . display_surface ( monitor_id )
} ;
match surface {
2026-04-14 20:05:26 -03:00
DisplaySurface ::Preview = > {
open_popout_window (
& app ,
preview ,
& state ,
& child_proc ,
& popouts ,
& widgets ,
monitor_id ,
) ;
widgets . status_label . set_text ( & format! (
2026-04-14 23:03:18 -03:00
" {} moved into its own window. " ,
2026-04-14 20:05:26 -03:00
widgets . display_panes [ monitor_id ] . title
) ) ;
}
DisplaySurface ::Window = > {
dock_display_to_preview (
& state ,
& child_proc ,
& popouts ,
& widgets ,
monitor_id ,
) ;
widgets . status_label . set_text ( & format! (
2026-04-14 23:03:18 -03:00
" {} returned to the launcher preview. " ,
2026-04-14 20:05:26 -03:00
widgets . display_panes [ monitor_id ] . title
) ) ;
}
}
} ) ;
}
2026-04-15 04:44:06 -03:00
{
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let widgets = widgets . clone ( ) ;
let key_controller = gtk ::EventControllerKey ::new ( ) ;
key_controller . connect_key_pressed ( move | _ , key , _ , _ | {
if ! state . borrow ( ) . swap_key_binding {
return glib ::Propagation ::Proceed ;
}
let Some ( swap_key ) = capture_swap_key ( key ) else {
widgets . status_label . set_text (
" That key is not a good swap shortcut. Try a letter, digit, function key, or navigation key. " ,
) ;
refresh_launcher_ui (
& widgets ,
& state . borrow ( ) ,
child_proc . borrow ( ) . is_some ( ) ,
) ;
return glib ::Propagation ::Stop ;
} ;
let relay_live = child_proc . borrow ( ) . is_some ( ) | | state . borrow ( ) . remote_active ;
{
let mut state = state . borrow_mut ( ) ;
2026-04-16 12:58:05 -03:00
state . complete_swap_key_binding ( swap_key . clone ( ) ) ;
2026-04-15 04:44:06 -03:00
}
let status_message = if relay_live {
format! (
" Swap key set to {}. Disconnect and reconnect the relay to use it live. " ,
toggle_key_label ( & swap_key )
)
} else {
format! (
" Swap key set to {}. The next relay launch will use it. " ,
toggle_key_label ( & swap_key )
)
} ;
widgets . status_label . set_text ( & status_message ) ;
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_proc . borrow ( ) . is_some ( ) ) ;
glib ::Propagation ::Stop
} ) ;
window . add_controller ( key_controller ) ;
}
2026-04-14 20:05:26 -03:00
{
let window = window . clone ( ) ;
let state = Rc ::clone ( & state ) ;
let child_proc = Rc ::clone ( & child_proc ) ;
let widgets = widgets . clone ( ) ;
let focus_signal_path = Rc ::clone ( & focus_signal_path ) ;
let input_state_path = Rc ::clone ( & input_state_path ) ;
2026-04-14 23:03:18 -03:00
let tests = Rc ::clone ( & tests ) ;
let server_entry = server_entry . clone ( ) ;
let server_addr_fallback = Rc ::clone ( & server_addr ) ;
2026-04-14 20:05:26 -03:00
let last_focus_marker =
Rc ::new ( RefCell ::new ( path_marker ( focus_signal_path . as_path ( ) ) ) ) ;
let last_state_marker =
Rc ::new ( RefCell ::new ( path_marker ( input_state_path . as_path ( ) ) ) ) ;
2026-04-14 23:03:18 -03:00
let power_request_in_flight = Rc ::clone ( & power_request_in_flight ) ;
2026-04-15 04:44:06 -03:00
let relay_request_in_flight = Rc ::clone ( & relay_request_in_flight ) ;
2026-04-15 04:11:47 -03:00
let preview = preview . clone ( ) ;
let power_tx = power_tx . clone ( ) ;
2026-04-16 21:18:34 -03:00
let caps_tx = caps_tx . clone ( ) ;
let caps_request_in_flight = Rc ::clone ( & caps_request_in_flight ) ;
let diagnostics_network = Rc ::clone ( & diagnostics_network ) ;
2026-04-17 06:14:54 -03:00
let diagnostics_process = Rc ::clone ( & diagnostics_process ) ;
2026-04-16 21:18:34 -03:00
let next_diagnostics_probe = Rc ::clone ( & next_diagnostics_probe ) ;
let next_diagnostics_sample = Rc ::clone ( & next_diagnostics_sample ) ;
2026-04-16 12:58:05 -03:00
let log_tx = log_tx . clone ( ) ;
2026-04-14 20:05:26 -03:00
glib ::timeout_add_local ( Duration ::from_millis ( 180 ) , move | | {
let child_running = reap_exited_child ( & child_proc ) ;
if ! child_running & & state . borrow ( ) . remote_active {
2026-04-15 04:11:47 -03:00
let power_mode = {
let mut state = state . borrow_mut ( ) ;
let _ = state . stop_remote ( ) ;
state . capture_power . mode . clone ( )
} ;
2026-04-16 12:58:05 -03:00
dock_all_displays_to_preview ( & state , & child_proc , & popouts , & widgets ) ;
window . present ( ) ;
2026-04-15 04:11:47 -03:00
if let Some ( preview ) = preview . as_ref ( ) {
preview . set_session_active ( false ) ;
}
let server_addr =
selected_server_addr ( & server_entry , server_addr_fallback . as_ref ( ) ) ;
2026-04-16 12:58:05 -03:00
if power_mode ! = " auto " {
widgets . status_label . set_text (
" Relay disconnected. Returning capture to automatic mode so it can fall back after the disconnect grace. " ,
) ;
request_capture_power_command (
power_tx . clone ( ) ,
server_addr . clone ( ) ,
CapturePowerCommand ::Auto ,
) ;
} else {
widgets
. status_label
. set_text ( disconnected_capture_note ( & power_mode ) ) ;
}
2026-04-15 04:11:47 -03:00
request_capture_power_refresh (
power_tx . clone ( ) ,
server_addr . clone ( ) ,
Duration ::from_millis ( 250 ) ,
) ;
request_capture_power_refresh (
power_tx . clone ( ) ,
server_addr ,
Duration ::from_secs ( 31 ) ,
) ;
2026-04-14 20:05:26 -03:00
}
let next_state_marker = path_marker ( input_state_path . as_path ( ) ) ;
let mut last_state = last_state_marker . borrow_mut ( ) ;
if next_state_marker > * last_state {
* last_state = next_state_marker ;
if let Some ( routing ) = read_input_routing_state ( input_state_path . as_path ( ) )
{
state . borrow_mut ( ) . set_routing ( routing ) ;
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_running ) ;
2026-04-16 12:58:05 -03:00
if matches! ( routing , InputRouting ::Remote ) {
present_popout_windows ( & popouts ) ;
} else {
window . present ( ) ;
}
2026-04-14 20:05:26 -03:00
}
}
let next_focus_marker = path_marker ( focus_signal_path . as_path ( ) ) ;
let mut last_focus = last_focus_marker . borrow_mut ( ) ;
if next_focus_marker > * last_focus {
* last_focus = next_focus_marker ;
state . borrow_mut ( ) . set_routing ( InputRouting ::Local ) ;
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_running ) ;
widgets
. status_label
2026-04-14 23:03:18 -03:00
. set_text ( " Local control restored and the launcher is focused. " ) ;
2026-04-14 20:05:26 -03:00
window . present ( ) ;
}
2026-04-15 04:44:06 -03:00
while let Ok ( message ) = relay_rx . try_recv ( ) {
relay_request_in_flight . set ( false ) ;
match message {
2026-04-16 12:58:05 -03:00
RelayMessage ::Spawned ( Ok ( mut child ) ) = > {
attach_child_log_streams ( & mut child , log_tx . clone ( ) ) ;
2026-04-15 04:44:06 -03:00
* child_proc . borrow_mut ( ) = Some ( child ) ;
2026-04-16 12:58:05 -03:00
{
let mut state = state . borrow_mut ( ) ;
state . set_server_available ( true ) ;
let _ = state . start_remote ( ) ;
}
2026-04-15 04:44:06 -03:00
let server_addr =
selected_server_addr ( & server_entry , server_addr_fallback . as_ref ( ) ) ;
if let Some ( preview ) = preview . as_ref ( ) {
preview . set_server_addr ( server_addr . clone ( ) ) ;
2026-04-16 12:58:05 -03:00
preview . set_session_active ( session_preview_active (
& state . borrow ( ) ,
child_proc . borrow ( ) . is_some ( ) ,
) ) ;
2026-04-15 04:44:06 -03:00
}
let routing = routing_name ( state . borrow ( ) . routing ) ;
let power_mode = state . borrow ( ) . capture_power . mode . clone ( ) ;
let message = match power_mode . as_str ( ) {
" forced-off " = > format! (
" Relay connected with inputs routed to {}, but capture is forced off. Return capture to Auto or Force On when you want remote video. " ,
routing
) ,
" forced-on " = > format! (
" Relay connected with inputs routed to {}. Capture is being held awake and the eye previews are coming online. " ,
routing
) ,
_ = > format! (
" Relay connected with inputs routed to {}. The eye previews will come up with the live session. " ,
routing
) ,
} ;
widgets . status_label . set_text ( & message ) ;
2026-04-16 12:58:05 -03:00
if matches! ( state . borrow ( ) . routing , InputRouting ::Remote ) {
present_popout_windows ( & popouts ) ;
}
2026-04-15 04:44:06 -03:00
request_capture_power_refresh (
power_tx . clone ( ) ,
server_addr . clone ( ) ,
Duration ::from_millis ( 250 ) ,
) ;
request_capture_power_refresh (
power_tx . clone ( ) ,
server_addr ,
Duration ::from_millis ( 1250 ) ,
) ;
}
RelayMessage ::Spawned ( Err ( err ) ) = > {
2026-04-16 12:58:05 -03:00
state . borrow_mut ( ) . set_server_available ( false ) ;
2026-04-15 04:44:06 -03:00
if let Some ( preview ) = preview . as_ref ( ) {
preview . set_session_active ( false ) ;
}
widgets
. status_label
. set_text ( & format! ( " Relay start failed: {err} " ) ) ;
}
}
}
2026-04-16 12:58:05 -03:00
while let Ok ( line ) = log_rx . try_recv ( ) {
append_session_log ( & widgets . session_log_buffer , & line ) ;
let mut end = widgets . session_log_buffer . end_iter ( ) ;
widgets
. session_log_view
. scroll_to_iter ( & mut end , 0.0 , false , 0.0 , 1.0 ) ;
}
2026-04-14 23:03:18 -03:00
while let Ok ( message ) = power_rx . try_recv ( ) {
power_request_in_flight . set ( false ) ;
match message {
2026-04-15 04:11:47 -03:00
PowerMessage ::Refresh ( Ok ( power ) ) = > {
2026-04-16 12:58:05 -03:00
{
let mut state = state . borrow_mut ( ) ;
state . set_server_available ( true ) ;
state . set_capture_power ( power ) ;
}
if let Some ( preview ) = preview . as_ref ( ) {
let preview_active = {
let state = state . borrow ( ) ;
session_preview_active (
& state ,
child_proc . borrow ( ) . is_some ( ) ,
)
} ;
preview . set_session_active ( preview_active ) ;
}
2026-04-14 23:03:18 -03:00
}
2026-04-15 04:11:47 -03:00
PowerMessage ::Refresh ( Err ( err ) ) = > {
2026-04-16 12:58:05 -03:00
{
let mut state = state . borrow_mut ( ) ;
state . set_server_available ( false ) ;
state . set_capture_power ( unavailable_capture_power ( err ) ) ;
}
if let Some ( preview ) = preview . as_ref ( ) {
let preview_active = {
let state = state . borrow ( ) ;
session_preview_active (
& state ,
child_proc . borrow ( ) . is_some ( ) ,
)
} ;
preview . set_session_active ( preview_active ) ;
}
2026-04-14 23:03:18 -03:00
}
PowerMessage ::Command ( Ok ( power ) ) = > {
2026-04-15 01:20:51 -03:00
let mode = power . mode . clone ( ) ;
2026-04-16 12:58:05 -03:00
{
let mut state = state . borrow_mut ( ) ;
state . set_server_available ( true ) ;
state . set_capture_power ( power ) ;
}
if let Some ( preview ) = preview . as_ref ( ) {
let preview_active = {
let state = state . borrow ( ) ;
session_preview_active (
& state ,
child_proc . borrow ( ) . is_some ( ) ,
)
} ;
preview . set_session_active ( preview_active ) ;
}
2026-04-15 01:20:51 -03:00
widgets . status_label . set_text ( match mode . as_str ( ) {
2026-04-15 01:57:14 -03:00
" forced-on " = > " Capture feeds forced on. Remote eyes stay awake even if previews or the relay stop. " ,
" forced-off " = > " Capture feeds forced off. Remote eye previews and session video stay dark until you switch back. " ,
_ = > " Capture feeds returned to automatic mode. Live previews and relay demand will wake them as needed. " ,
2026-04-14 23:03:18 -03:00
} ) ;
}
PowerMessage ::Command ( Err ( err ) ) = > {
2026-04-16 12:58:05 -03:00
let mut state = state . borrow_mut ( ) ;
state . set_server_available ( false ) ;
state . set_capture_power ( unavailable_capture_power ( err . clone ( ) ) ) ;
2026-04-14 23:03:18 -03:00
widgets
. status_label
. set_text ( & format! ( " Capture power update failed: {err} " ) ) ;
}
}
}
2026-04-16 12:58:05 -03:00
while let Ok ( message ) = caps_rx . try_recv ( ) {
2026-04-16 21:18:34 -03:00
caps_request_in_flight . set ( false ) ;
2026-04-16 12:58:05 -03:00
match message {
2026-04-16 21:18:34 -03:00
CapsMessage ::Refresh ( probe_result ) = > {
diagnostics_network . borrow_mut ( ) . record ( & probe_result ) ;
let caps = probe_result . caps ;
2026-04-16 21:36:16 -03:00
let rebind_preview = eye_caps_changed ( & state . borrow ( ) , & caps ) ;
2026-04-16 15:59:42 -03:00
{
let mut state = state . borrow_mut ( ) ;
2026-04-16 21:18:34 -03:00
if probe_result . reachable {
state . set_server_available ( true ) ;
} else if child_proc . borrow ( ) . is_none ( ) {
state . set_server_available ( false ) ;
}
2026-04-16 15:59:42 -03:00
state . set_server_version ( caps . server_version . clone ( ) ) ;
}
2026-04-16 12:58:05 -03:00
if let ( Some ( width ) , Some ( height ) ) =
( caps . eye_width , caps . eye_height )
{
2026-04-19 03:28:23 -03:00
let fps = caps
. eye_fps
. unwrap_or ( crate ::launcher ::state ::PreviewSourceSize ::default ( ) . fps ) ;
2026-04-16 21:36:16 -03:00
{
let mut state = state . borrow_mut ( ) ;
state . set_preview_source_profile ( width , height , fps ) ;
}
if rebind_preview & & let Some ( preview ) = preview . as_ref ( ) {
2026-04-18 02:26:24 -03:00
sync_preview_profiles ( preview , & widgets , & popouts , & state . borrow ( ) ) ;
2026-04-16 12:58:05 -03:00
}
refresh_eye_feed_controls ( & widgets , & state . borrow ( ) ) ;
} else {
refresh_eye_feed_controls ( & widgets , & state . borrow ( ) ) ;
}
}
}
}
while let Ok ( message ) = clipboard_rx . try_recv ( ) {
match message {
ClipboardMessage ::Finished ( Ok ( detail ) ) = > {
widgets . status_label . set_text ( & format! ( " ✨ {detail} " ) ) ;
}
ClipboardMessage ::Finished ( Err ( err ) ) = > {
widgets
. status_label
. set_text ( & format! ( " Clipboard send failed: {err} " ) ) ;
}
}
}
2026-04-16 21:18:34 -03:00
let now = Instant ::now ( ) ;
2026-04-20 01:41:57 -03:00
let child_running = child_proc . borrow ( ) . is_some ( ) ;
if now > = next_power_probe . get ( )
& & ! power_request_in_flight . get ( )
& & ( child_running
| | state . borrow ( ) . capture_power . enabled
| | state . borrow ( ) . remote_active )
{
power_request_in_flight . set ( true ) ;
let server_addr =
selected_server_addr ( & server_entry , server_addr_fallback . as_ref ( ) ) ;
request_capture_power_refresh ( power_tx . clone ( ) , server_addr , Duration ::ZERO ) ;
next_power_probe . set ( now + Duration ::from_secs ( 2 ) ) ;
}
2026-04-16 21:18:34 -03:00
if now > = next_diagnostics_probe . get ( ) & & ! caps_request_in_flight . get ( ) {
caps_request_in_flight . set ( true ) ;
let server_addr =
selected_server_addr ( & server_entry , server_addr_fallback . as_ref ( ) ) ;
request_handshake_caps ( caps_tx . clone ( ) , server_addr , Duration ::ZERO ) ;
next_diagnostics_probe . set ( now + Duration ::from_secs ( 2 ) ) ;
}
if now > = next_diagnostics_sample . get ( ) {
let network = diagnostics_network . borrow_mut ( ) . snapshot ( ) ;
2026-04-17 06:14:54 -03:00
let client_process_cpu_pct = diagnostics_process
. borrow_mut ( )
. sample_percent ( )
. unwrap_or ( 0.0 ) ;
2026-04-16 21:18:34 -03:00
record_diagnostics_sample (
& widgets ,
& state . borrow ( ) ,
preview . as_ref ( ) . map ( | preview | preview . as_ref ( ) ) ,
network ,
2026-04-17 06:14:54 -03:00
client_process_cpu_pct ,
2026-04-16 21:18:34 -03:00
) ;
next_diagnostics_sample . set ( now + Duration ::from_secs ( 1 ) ) ;
}
2026-04-14 20:05:26 -03:00
refresh_launcher_ui ( & widgets , & state . borrow ( ) , child_running ) ;
2026-04-14 23:03:18 -03:00
refresh_test_buttons ( & widgets , & mut tests . borrow_mut ( ) ) ;
2026-04-14 20:05:26 -03:00
glib ::ControlFlow ::Continue
} ) ;
}
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 ( ( ) )
}
2026-04-18 03:25:20 -03:00
#[ cfg(all(test, not(coverage))) ]
mod tests {
use super ::apply_preview_profiles ;
use crate ::launcher ::preview ::{ LauncherPreview , PreviewSurface } ;
2026-04-19 03:28:23 -03:00
use crate ::launcher ::state ::{ CaptureSizePreset , FeedSourcePreset , LauncherState } ;
2026-04-18 03:25:20 -03:00
#[ test ]
fn fresh_preview_bootstrap_is_overridden_by_launcher_state_profiles ( ) {
let preview = LauncherPreview ::new ( " http://127.0.0.1:1 " . to_string ( ) ) . unwrap ( ) ;
let state = LauncherState ::default ( ) ;
let bootstrap = preview . profile_for_test ( 1 , PreviewSurface ::Inline ) . unwrap ( ) ;
2026-04-19 03:28:23 -03:00
assert_eq! ( bootstrap . 0 , 0 ) ;
assert_eq! ( bootstrap . 3 , 1920 ) ;
assert_eq! ( bootstrap . 4 , 1080 ) ;
assert_eq! ( bootstrap . 5 , 60 ) ;
assert_eq! ( bootstrap . 6 , 18_000 ) ;
2026-04-18 03:25:20 -03:00
apply_preview_profiles ( & preview , & state ) ;
let inline = preview . profile_for_test ( 1 , PreviewSurface ::Inline ) . unwrap ( ) ;
2026-04-19 03:28:23 -03:00
assert_eq! ( inline . 0 , 1 ) ;
assert_eq! ( inline . 3 , 1920 ) ;
assert_eq! ( inline . 4 , 1080 ) ;
assert_eq! ( inline . 5 , 60 ) ;
assert_eq! ( inline . 6 , 18_000 ) ;
2026-04-18 03:25:20 -03:00
let window = preview . profile_for_test ( 1 , PreviewSurface ::Window ) . unwrap ( ) ;
2026-04-19 03:28:23 -03:00
assert_eq! ( window . 0 , 1 ) ;
assert_eq! ( window . 3 , 1920 ) ;
assert_eq! ( window . 4 , 1080 ) ;
assert_eq! ( window . 5 , 60 ) ;
assert_eq! ( window . 6 , 18_000 ) ;
2026-04-18 03:25:20 -03:00
preview . shutdown_all ( ) ;
}
#[ test ]
fn source_preview_profile_stays_honest_after_apply ( ) {
let preview = LauncherPreview ::new ( " http://127.0.0.1:1 " . to_string ( ) ) . unwrap ( ) ;
let mut state = LauncherState ::default ( ) ;
2026-04-19 03:28:23 -03:00
state . set_capture_size_preset ( 1 , CaptureSizePreset ::P1080 ) ;
2026-04-18 03:25:20 -03:00
apply_preview_profiles ( & preview , & state ) ;
let inline = preview . profile_for_test ( 1 , PreviewSurface ::Inline ) . unwrap ( ) ;
2026-04-19 03:28:23 -03:00
assert_eq! ( inline . 0 , 1 ) ;
assert_eq! ( inline . 3 , 1920 ) ;
assert_eq! ( inline . 4 , 1080 ) ;
assert_eq! ( inline . 5 , 60 ) ;
assert_eq! ( inline . 6 , 18_000 ) ;
preview . shutdown_all ( ) ;
}
#[ test ]
fn mirrored_preview_profile_keeps_its_own_feed_id_but_uses_the_other_source ( ) {
let preview = LauncherPreview ::new ( " http://127.0.0.1:1 " . to_string ( ) ) . unwrap ( ) ;
let mut state = LauncherState ::default ( ) ;
state . set_feed_source_preset ( 0 , FeedSourcePreset ::OtherEye ) ;
apply_preview_profiles ( & preview , & state ) ;
let inline = preview . profile_for_test ( 0 , PreviewSurface ::Inline ) . unwrap ( ) ;
let window = preview . profile_for_test ( 0 , PreviewSurface ::Window ) . unwrap ( ) ;
assert_eq! ( inline . 0 , 1 ) ;
assert_eq! ( window . 0 , 1 ) ;
preview . shutdown_all ( ) ;
}
2026-04-19 11:42:41 -03:00
#[ test ]
fn mirrored_preview_profile_inherits_the_source_eye_mode ( ) {
let preview = LauncherPreview ::new ( " http://127.0.0.1:1 " . to_string ( ) ) . unwrap ( ) ;
let mut state = LauncherState ::default ( ) ;
state . set_feed_source_preset ( 0 , FeedSourcePreset ::OtherEye ) ;
state . set_capture_size_preset ( 1 , CaptureSizePreset ::P720 ) ;
apply_preview_profiles ( & preview , & state ) ;
let inline = preview . profile_for_test ( 0 , PreviewSurface ::Inline ) . unwrap ( ) ;
let window = preview . profile_for_test ( 0 , PreviewSurface ::Window ) . unwrap ( ) ;
assert_eq! ( inline . 0 , 1 ) ;
assert_eq! ( window . 0 , 1 ) ;
assert_eq! ( inline . 3 , 1280 ) ;
assert_eq! ( inline . 4 , 720 ) ;
assert_eq! ( inline . 5 , 60 ) ;
assert_eq! ( inline . 6 , 12_000 ) ;
assert_eq! ( window . 3 , 1280 ) ;
assert_eq! ( window . 4 , 720 ) ;
assert_eq! ( window . 5 , 60 ) ;
assert_eq! ( window . 6 , 12_000 ) ;
preview . shutdown_all ( ) ;
}
2026-04-19 03:28:23 -03:00
#[ test ]
fn off_preview_profile_disables_both_surfaces_instead_of_leaving_idle_feeds_running ( ) {
let preview = LauncherPreview ::new ( " http://127.0.0.1:1 " . to_string ( ) ) . unwrap ( ) ;
let mut state = LauncherState ::default ( ) ;
state . set_feed_source_preset ( 0 , FeedSourcePreset ::Off ) ;
apply_preview_profiles ( & preview , & state ) ;
assert_eq! (
preview . feed_disabled_for_test ( 0 , PreviewSurface ::Inline ) ,
Some ( true )
) ;
assert_eq! (
preview . feed_disabled_for_test ( 0 , PreviewSurface ::Window ) ,
Some ( true )
) ;
assert_eq! (
preview . feed_disabled_for_test ( 1 , PreviewSurface ::Inline ) ,
Some ( false )
) ;
2026-04-18 03:25:20 -03:00
preview . shutdown_all ( ) ;
}
}
2026-04-13 23:11:35 -03:00
#[ cfg(all(test, coverage)) ]
mod tests {
2026-04-16 12:58:05 -03:00
use super ::{ run_gui_launcher , session_preview_active } ;
use crate ::launcher ::state ::{ CapturePowerStatus , LauncherState } ;
2026-04-13 23:11:35 -03:00
#[ test ]
fn coverage_stub_returns_ok ( ) {
assert! ( run_gui_launcher ( " http://127.0.0.1:50051 " . to_string ( ) ) . is_ok ( ) ) ;
}
2026-04-16 12:58:05 -03:00
#[ test ]
fn session_preview_stays_idle_when_capture_is_forced_off ( ) {
let mut state = LauncherState ::new ( ) ;
state . start_remote ( ) ;
state . set_capture_power ( CapturePowerStatus {
available : true ,
enabled : false ,
unit : " relay.service " . to_string ( ) ,
detail : " inactive/dead " . to_string ( ) ,
active_leases : 1 ,
mode : " forced-off " . to_string ( ) ,
2026-04-20 01:06:50 -03:00
detected_devices : 0 ,
2026-04-16 12:58:05 -03:00
} ) ;
assert! ( ! session_preview_active ( & state , true ) ) ;
}
2026-04-13 23:11:35 -03:00
}