feat(launcher): guide staging flow and capture modes

This commit is contained in:
Brad Stein 2026-04-15 01:57:14 -03:00
parent e15afc2ebd
commit e7dcfd2fd5
6 changed files with 553 additions and 254 deletions

View File

@ -21,6 +21,7 @@ pub enum DeviceTestKind {
Speaker,
}
#[derive(Default)]
pub struct DeviceTestController {
camera: Option<LocalCameraPreview>,
selected_camera: Option<String>,
@ -28,17 +29,6 @@ pub struct DeviceTestController {
speaker: Option<Child>,
}
impl Default for DeviceTestController {
fn default() -> Self {
Self {
camera: None,
selected_camera: None,
microphone: None,
speaker: None,
}
}
}
impl DeviceTestController {
pub fn new() -> Self {
Self::default()
@ -227,7 +217,9 @@ impl LocalCameraPreview {
}
self.set_status(match self.selected_device.as_deref() {
Some(camera) => format!("Selected {camera}. Click Start Preview to verify it here."),
Some(camera) => format!(
"Selected {camera}. Start Preview to confirm framing here before you launch the relay."
),
None => CAMERA_PREVIEW_IDLE.to_string(),
});
Ok(())
@ -255,7 +247,7 @@ impl LocalCameraPreview {
let running = Arc::clone(&self.running);
let token = generation.fetch_add(1, Ordering::AcqRel) + 1;
running.store(true, Ordering::Release);
self.set_status(format!("Starting preview for {selected}..."));
self.set_status(format!("Starting local preview for {selected}..."));
std::thread::spawn(move || {
if let Err(err) = run_camera_preview_feed(
@ -266,12 +258,11 @@ impl LocalCameraPreview {
status_text.clone(),
generation.clone(),
running.clone(),
) {
if generation.load(Ordering::Acquire) == token {
running.store(false, Ordering::Release);
if let Ok(mut status) = status_text.lock() {
*status = format!("Camera preview failed: {err}");
}
) && generation.load(Ordering::Acquire) == token
{
running.store(false, Ordering::Release);
if let Ok(mut status) = status_text.lock() {
*status = format!("Camera preview failed: {err}");
}
}
});
@ -285,7 +276,9 @@ impl LocalCameraPreview {
*latest = None;
}
self.set_status(match self.selected_device.as_deref() {
Some(camera) => format!("Preview stopped. {camera} is still selected."),
Some(camera) => {
format!("Local preview stopped. {camera} stays selected for the next relay launch.")
}
None => CAMERA_PREVIEW_IDLE.to_string(),
});
}
@ -327,16 +320,15 @@ fn run_camera_preview_feed(
.context("starting in-launcher camera preview pipeline")?;
if let Ok(mut status) = status_text.lock() {
*status = format!("Previewing {selected} locally.");
*status = format!("Local preview live for {selected}.");
}
while running.load(Ordering::Acquire) && generation.load(Ordering::Acquire) == token {
if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(250)) {
if let Some(frame) = sample_to_frame(&sample) {
if let Ok(mut slot) = latest.lock() {
*slot = Some(frame);
}
}
if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(250))
&& let Some(frame) = sample_to_frame(&sample)
&& let Ok(mut slot) = latest.lock()
{
*slot = Some(frame);
}
}
@ -363,9 +355,9 @@ fn build_camera_preview_pipeline(device: &str) -> Result<(gst::Pipeline, gst_app
.expect("camera preview appsink");
appsink.set_caps(Some(
&gst::Caps::builder("video/x-raw")
.field("format", &"RGBA")
.field("width", &CAMERA_PREVIEW_WIDTH)
.field("height", &CAMERA_PREVIEW_HEIGHT)
.field("format", "RGBA")
.field("width", CAMERA_PREVIEW_WIDTH)
.field("height", CAMERA_PREVIEW_HEIGHT)
.build(),
));
Ok((pipeline, appsink))

View File

@ -3,7 +3,7 @@ use anyhow::Result;
#[cfg(not(coverage))]
use {
super::clipboard::send_clipboard_to_remote,
super::device_test::DeviceTestController,
super::device_test::{DeviceTestController, DeviceTestKind},
super::devices::DeviceCatalog,
super::diagnostics::quality_probe_command,
super::launcher_focus_signal_path,
@ -121,11 +121,18 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let camera_combo_read = camera_combo.clone();
camera_combo.connect_changed(move |_| {
let selected = selected_combo_value(&camera_combo_read);
let preview_was_running =
tests.borrow_mut().is_running(DeviceTestKind::Camera);
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}"));
} else if preview_was_running {
widgets.status_label.set_text(&format!(
"Local camera preview switched to {}.",
selected.as_deref().unwrap_or("auto")
));
}
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
@ -136,13 +143,20 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let state = Rc::clone(&state);
let widgets = widgets.clone();
let child_proc = Rc::clone(&child_proc);
let tests = Rc::clone(&tests);
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));
if tests.borrow_mut().is_running(DeviceTestKind::Microphone) {
widgets.status_label.set_text(
"Microphone selection changed. Restart Monitor Mic to audition the new input.",
);
}
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
});
}
@ -150,13 +164,23 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let state = Rc::clone(&state);
let widgets = widgets.clone();
let child_proc = Rc::clone(&child_proc);
let tests = Rc::clone(&tests);
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));
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.",
);
}
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
});
}
@ -203,10 +227,23 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
Ok(child) => {
*child_proc.borrow_mut() = Some(child);
let _ = state.borrow_mut().start_remote();
widgets_handle.status_label.set_text(&format!(
"Relay started. Inputs are routed to {}.",
routing_name(state.borrow().routing)
));
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 started 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 started with inputs routed to {}. Capture is being held awake for staging.",
routing
),
_ => format!(
"Relay started with inputs routed to {}. Capture will wake automatically for previews and live session demand.",
routing
),
};
widgets_handle.status_label.set_text(&message);
}
Err(err) => {
widgets_handle
@ -231,7 +268,9 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
stop_button.connect_clicked(move |_| {
stop_child_process(&child_proc);
let _ = state.borrow_mut().stop_remote();
widgets_handle.status_label.set_text("Relay stopped.");
widgets_handle
.status_label
.set_text("Relay stopped. Local staging remains available.");
refresh_launcher_ui(&widgets_handle, &state.borrow(), false);
});
}
@ -351,8 +390,8 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
&widgets_handle,
&mut tests.borrow_mut(),
result,
"Camera preview started inside the launcher.",
"Camera preview stopped.",
"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.",
);
});
}
@ -374,7 +413,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
&widgets_handle,
&mut tests.borrow_mut(),
result,
"Microphone monitor started.",
"Microphone monitor started locally through the selected speaker.",
"Microphone monitor stopped.",
);
});
@ -394,7 +433,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
&widgets_handle,
&mut tests.borrow_mut(),
result,
"Speaker test tone started.",
"Speaker tone started locally.",
"Speaker test tone stopped.",
);
});
@ -547,7 +586,9 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let child_running = reap_exited_child(&child_proc);
if !child_running && state.borrow().remote_active {
let _ = state.borrow_mut().stop_remote();
widgets.status_label.set_text("Relay ended.");
widgets
.status_label
.set_text("Relay ended. Local staging remains available.");
}
let next_state_marker = path_marker(input_state_path.as_path());
@ -593,9 +634,9 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let mode = power.mode.clone();
state.borrow_mut().set_capture_power(power);
widgets.status_label.set_text(match mode.as_str() {
"forced-on" => "Capture feeds forced on.",
"forced-off" => "Capture feeds forced off.",
_ => "Capture feeds returned to automatic mode.",
"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.",
});
}
PowerMessage::Command(Err(err)) => {

View File

@ -39,6 +39,10 @@ pub struct LauncherWidgets {
pub status_label: gtk::Label,
pub summary: SummaryWidgets,
pub power_detail: gtk::Label,
pub launch_plan_title: gtk::Label,
pub launch_plan_summary: gtk::Label,
pub launch_plan_detail: gtk::Label,
pub local_test_detail: gtk::Label,
pub display_panes: [DisplayPaneWidgets; 2],
pub start_button: gtk::Button,
pub stop_button: gtk::Button,
@ -151,8 +155,14 @@ pub fn build_launcher_view(
));
let start_button = gtk::Button::with_label("Start Relay");
start_button.add_css_class("suggested-action");
start_button.set_tooltip_text(Some(
"Launch the relay using the staged devices and current input routing.",
));
let stop_button = gtk::Button::with_label("Stop Relay");
stop_button.add_css_class("destructive-action");
stop_button.set_tooltip_text(Some(
"Stop the live relay session. Local staging and previews stay available.",
));
server_row.append(&server_entry);
server_row.append(&start_button);
server_row.append(&stop_button);
@ -225,7 +235,7 @@ pub fn build_launcher_view(
let (devices_panel, devices_body) = build_panel("Device Staging");
let devices_intro = gtk::Label::new(Some(
"Lock in the exact local camera, microphone, and speaker you want before you launch the relay.",
"Choose the exact local camera, microphone, and speaker the next relay launch should inherit.",
));
devices_intro.add_css_class("dim-label");
devices_intro.set_wrap(true);
@ -320,6 +330,37 @@ pub fn build_launcher_view(
devices_body.append(&preview_shell);
sidebar.append(&devices_panel);
let (plan_panel, plan_body) = build_panel("Launch Plan");
let launch_plan_title = gtk::Label::new(Some("Stage locally, then start the relay."));
launch_plan_title.add_css_class("title-4");
launch_plan_title.set_halign(gtk::Align::Start);
launch_plan_title.set_wrap(true);
let launch_plan_summary =
gtk::Label::new(Some("Camera: auto\nMicrophone: auto\nSpeaker: auto"));
launch_plan_summary.add_css_class("launch-plan-summary");
launch_plan_summary.set_halign(gtk::Align::Start);
launch_plan_summary.set_xalign(0.0);
launch_plan_summary.set_wrap(true);
let local_test_detail = gtk::Label::new(Some(
"Local checks are idle. Use Start Preview, Monitor Mic, or Play Tone before you launch.",
));
local_test_detail.add_css_class("dim-label");
local_test_detail.set_halign(gtk::Align::Start);
local_test_detail.set_xalign(0.0);
local_test_detail.set_wrap(true);
let launch_plan_detail = gtk::Label::new(Some(
"Automatic capture mode will wake the remote feeds when previews or the live relay ask for them.",
));
launch_plan_detail.add_css_class("dim-label");
launch_plan_detail.set_halign(gtk::Align::Start);
launch_plan_detail.set_xalign(0.0);
launch_plan_detail.set_wrap(true);
plan_body.append(&launch_plan_title);
plan_body.append(&launch_plan_summary);
plan_body.append(&local_test_detail);
plan_body.append(&launch_plan_detail);
sidebar.append(&plan_panel);
let (actions_panel, actions_body) = build_panel("Remote Actions");
let actions_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
let clipboard_button = gtk::Button::with_label("Send Clipboard");
@ -336,11 +377,18 @@ pub fn build_launcher_view(
sidebar.append(&actions_panel);
let stage_header = gtk::Box::new(gtk::Orientation::Horizontal, 8);
let stage_title = gtk::Label::new(Some("Remote Eyes"));
let stage_title = gtk::Label::new(Some("Remote Eye Feeds"));
stage_title.add_css_class("title-4");
stage_title.set_halign(gtk::Align::Start);
stage_header.append(&stage_title);
let stage_note = gtk::Label::new(Some(
"These are the live server-side eye feeds. In Auto mode, open eye previews and active relay sessions count as capture demand.",
));
stage_note.add_css_class("dim-label");
stage_note.set_wrap(true);
stage_note.set_xalign(0.0);
stage.append(&stage_header);
stage.append(&stage_note);
let display_row = gtk::Box::new(gtk::Orientation::Horizontal, 16);
display_row.set_hexpand(true);
@ -387,6 +435,10 @@ pub fn build_launcher_view(
shortcut_value,
},
power_detail,
launch_plan_title,
launch_plan_summary,
launch_plan_detail,
local_test_detail,
display_panes: [left_pane.clone(), right_pane.clone()],
start_button: start_button.clone(),
stop_button: stop_button.clone(),
@ -482,6 +534,13 @@ pub fn install_css(window: &gtk::ApplicationWindow) {
label.status-line {
opacity: 0.88;
}
label.launch-plan-summary {
font-family: monospace;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
padding: 12px;
}
entry.server-entry {
min-height: 38px;
}
@ -489,6 +548,11 @@ pub fn install_css(window: &gtk::ApplicationWindow) {
min-height: 36px;
padding: 0 14px;
}
button.pill-toggle-active {
background: rgba(91, 179, 162, 0.2);
border-color: rgba(91, 179, 162, 0.45);
font-weight: 700;
}
"#,
);
if let Some(display) = gtk::gdk::Display::default() {

View File

@ -52,7 +52,21 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
widgets
.power_detail
.set_text(&capture_power_detail(&state.capture_power));
widgets
.launch_plan_title
.set_text(&launch_plan_title(state, child_running));
widgets
.launch_plan_summary
.set_text(&launch_plan_summary(state));
widgets
.launch_plan_detail
.set_text(&launch_plan_detail(state, child_running));
widgets.start_button.set_label(if child_running {
"Relay Running"
} else {
"Start Relay"
});
widgets.start_button.set_sensitive(!child_running);
widgets.stop_button.set_sensitive(child_running);
widgets.clipboard_button.set_sensitive(child_running);
@ -71,6 +85,7 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
widgets.power_off_button.set_sensitive(
power_available && !matches!(state.capture_power.mode.as_str(), "forced-off"),
);
sync_power_mode_button_styles(widgets, state.capture_power.mode.as_str());
for monitor_id in 0..2 {
refresh_display_pane(
@ -81,27 +96,32 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
}
pub fn refresh_test_buttons(widgets: &LauncherWidgets, tests: &mut DeviceTestController) {
widgets
.camera_test_button
.set_label(if tests.is_running(DeviceTestKind::Camera) {
"Stop Preview"
} else {
"Start Preview"
});
let camera_running = tests.is_running(DeviceTestKind::Camera);
let microphone_running = tests.is_running(DeviceTestKind::Microphone);
let speaker_running = tests.is_running(DeviceTestKind::Speaker);
widgets.camera_test_button.set_label(if camera_running {
"Stop Preview"
} else {
"Start Preview"
});
widgets
.microphone_test_button
.set_label(if tests.is_running(DeviceTestKind::Microphone) {
.set_label(if microphone_running {
"Stop Monitor"
} else {
"Monitor Mic"
});
widgets
.speaker_test_button
.set_label(if tests.is_running(DeviceTestKind::Speaker) {
"Stop Tone"
} else {
"Play Tone"
});
widgets.speaker_test_button.set_label(if speaker_running {
"Stop Tone"
} else {
"Play Tone"
});
widgets.local_test_detail.set_text(&local_test_detail(
camera_running,
microphone_running,
speaker_running,
));
}
pub fn update_test_action_result(
@ -140,7 +160,7 @@ pub fn open_popout_window(
let window = gtk::ApplicationWindow::builder()
.application(app)
.title(&format!(
.title(format!(
"Lesavka {}",
widgets.display_panes[monitor_id].title
))
@ -310,6 +330,137 @@ pub fn capture_power_detail(power: &CapturePowerStatus) -> String {
}
}
/// Highlights the currently active capture mode so it reads like a segmented control.
fn sync_power_mode_button_styles(widgets: &LauncherWidgets, mode: &str) {
for button in [
&widgets.power_auto_button,
&widgets.power_on_button,
&widgets.power_off_button,
] {
button.remove_css_class("pill-toggle-active");
}
match mode {
"forced-on" => widgets.power_on_button.add_css_class("pill-toggle-active"),
"forced-off" => widgets.power_off_button.add_css_class("pill-toggle-active"),
_ => widgets
.power_auto_button
.add_css_class("pill-toggle-active"),
}
}
/// Summarizes the current staging state as a short operator-facing heading.
fn launch_plan_title(state: &LauncherState, child_running: bool) -> String {
if child_running || state.remote_active {
return match state.capture_power.mode.as_str() {
"forced-off" => "Relay live, but capture is intentionally dark.".to_string(),
"forced-on" => "Relay live with capture held awake.".to_string(),
_ => "Relay live with automatic capture management.".to_string(),
};
}
match state.capture_power.mode.as_str() {
"forced-off" => "Staging mode is holding capture off.".to_string(),
"forced-on" => "Capture is pre-warmed for staging.".to_string(),
_ => "Stage locally, then start the relay.".to_string(),
}
}
/// Shows the exact devices the next relay launch will inherit.
fn launch_plan_summary(state: &LauncherState) -> String {
format!(
"Camera: {}\nMicrophone: {}\nSpeaker: {}",
selected_device_label(state.devices.camera.as_deref()),
selected_device_label(state.devices.microphone.as_deref()),
selected_device_label(state.devices.speaker.as_deref())
)
}
/// Explains the consequence of the current capture and session state.
fn launch_plan_detail(state: &LauncherState, child_running: bool) -> String {
if child_running || state.remote_active {
return match state.capture_power.mode.as_str() {
"forced-off" => format!(
"Inputs are routed to {}. Return capture to Auto or Force On when you want the remote eyes and session video to wake up.",
capitalize(routing_name(state.routing))
),
"forced-on" => format!(
"Inputs are routed to {}. The relay host is keeping the capture feeds up even without preview demand.",
capitalize(routing_name(state.routing))
),
_ => format!(
"Inputs are routed to {}. Live eye previews and session demand will wake capture automatically as needed.",
capitalize(routing_name(state.routing))
),
};
}
if !state.capture_power.available {
return format!(
"Capture power status from {} is unavailable right now. You can still stage devices locally before you try the relay host again.",
state.capture_power.unit
);
}
match state.capture_power.mode.as_str() {
"forced-off" => {
"Remote eye previews and the next relay session will stay dark until you return to Auto or Force On."
.to_string()
}
"forced-on" => {
"The relay host is already holding capture awake, which is useful for preflight framing checks before the session starts."
.to_string()
}
_ => {
"Automatic capture mode wakes the remote feeds only while eye previews or the live relay session actually need them."
.to_string()
}
}
}
/// Reports which local staging checks are active right now.
fn local_test_detail(
camera_running: bool,
microphone_running: bool,
speaker_running: bool,
) -> String {
let mut active = Vec::new();
if camera_running {
active.push("camera preview");
}
if microphone_running {
active.push("mic monitor");
}
if speaker_running {
active.push("speaker tone");
}
if active.is_empty() {
"Local checks are idle. Use Start Preview, Monitor Mic, or Play Tone before you launch."
.to_string()
} else {
format!(
"Local checks running: {}. Stop them whenever staging is complete.",
active.join(", ")
)
}
}
/// Formats a selected device for the launch-plan summary.
fn selected_device_label(value: Option<&str>) -> String {
value
.map(compact_device_name)
.unwrap_or_else(|| "auto".to_string())
}
/// Prefer the basename for `/dev/...` entries while keeping Pulse names intact.
fn compact_device_name(value: &str) -> String {
let trimmed = value.trim();
if trimmed.is_empty() {
return "auto".to_string();
}
trimmed.rsplit('/').next().unwrap_or(trimmed).to_string()
}
pub fn capitalize(value: &str) -> String {
let mut chars = value.chars();
match chars.next() {
@ -455,3 +606,55 @@ pub fn next_input_routing(routing: InputRouting) -> InputRouting {
InputRouting::Local => InputRouting::Remote,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn launch_plan_summary_compacts_selected_devices() {
let mut state = LauncherState::new();
state.select_camera(Some(
"/dev/v4l/by-id/usb-Logitech_C920-video-index0".to_string(),
));
state.select_microphone(Some("alsa_input.usb-focusrite".to_string()));
state.select_speaker(Some("alsa_output.studio".to_string()));
let summary = launch_plan_summary(&state);
assert!(summary.contains("usb-Logitech_C920-video-index0"));
assert!(summary.contains("alsa_input.usb-focusrite"));
assert!(summary.contains("alsa_output.studio"));
assert!(!summary.contains("/dev/v4l/by-id/"));
}
#[test]
fn launch_plan_detail_calls_out_forced_off_sessions() {
let mut state = LauncherState::new();
state.set_capture_power(CapturePowerStatus {
available: true,
enabled: false,
unit: "relay.service".to_string(),
detail: "inactive/dead".to_string(),
active_leases: 0,
mode: "forced-off".to_string(),
});
state.start_remote();
let detail = launch_plan_detail(&state, true);
assert!(detail.contains("Return capture to Auto or Force On"));
}
#[test]
fn local_test_detail_mentions_idle_and_running_modes() {
assert!(local_test_detail(false, false, false).contains("idle"));
let running = local_test_detail(true, true, false);
assert!(running.contains("camera preview"));
assert!(running.contains("mic monitor"));
}
#[test]
fn compact_device_name_prefers_basename_when_available() {
assert_eq!(compact_device_name("/dev/video0"), "video0");
assert_eq!(compact_device_name("alsa_input.usb"), "alsa_input.usb");
}
}

View File

@ -1,273 +1,272 @@
{
"files": {
"client/src/app.rs": {
"loc": 546,
"clippy_warnings": 42,
"doc_debt": 10
"doc_debt": 10,
"loc": 546
},
"client/src/app_support.rs": {
"loc": 128,
"clippy_warnings": 0,
"doc_debt": 3,
"clippy_warnings": 0
"loc": 128
},
"client/src/handshake.rs": {
"loc": 194,
"clippy_warnings": 0,
"doc_debt": 3,
"clippy_warnings": 0
"loc": 194
},
"client/src/input/camera.rs": {
"loc": 372,
"clippy_warnings": 38,
"doc_debt": 7
"doc_debt": 7,
"loc": 372
},
"client/src/input/inputs.rs": {
"loc": 673,
"clippy_warnings": 42,
"doc_debt": 16
"doc_debt": 16,
"loc": 673
},
"client/src/input/keyboard.rs": {
"loc": 580,
"clippy_warnings": 24,
"doc_debt": 18
"doc_debt": 18,
"loc": 580
},
"client/src/input/keymap.rs": {
"loc": 196,
"clippy_warnings": 8,
"doc_debt": 0
"doc_debt": 0,
"loc": 196
},
"client/src/input/microphone.rs": {
"loc": 166,
"clippy_warnings": 17,
"doc_debt": 2
"doc_debt": 2,
"loc": 166
},
"client/src/input/mod.rs": {
"loc": 8,
"clippy_warnings": 0,
"doc_debt": 0,
"clippy_warnings": 0
"loc": 8
},
"client/src/input/mouse.rs": {
"loc": 317,
"clippy_warnings": 40,
"doc_debt": 8
"doc_debt": 8,
"loc": 317
},
"client/src/launcher/clipboard.rs": {
"loc": 177,
"clippy_warnings": 2,
"doc_debt": 1
"doc_debt": 1,
"loc": 177
},
"client/src/launcher/device_test.rs": {
"loc": 454,
"clippy_warnings": 36,
"doc_debt": 20
"clippy_warnings": 22,
"doc_debt": 20,
"loc": 446
},
"client/src/launcher/devices.rs": {
"loc": 158,
"clippy_warnings": 6,
"doc_debt": 3
"doc_debt": 3,
"loc": 158
},
"client/src/launcher/diagnostics.rs": {
"loc": 175,
"clippy_warnings": 17,
"doc_debt": 3
"doc_debt": 3,
"loc": 175
},
"client/src/launcher/mod.rs": {
"loc": 195,
"clippy_warnings": 6,
"doc_debt": 4
"doc_debt": 4,
"loc": 195
},
"client/src/launcher/power.rs": {
"loc": 69,
"clippy_warnings": 0,
"doc_debt": 1,
"clippy_warnings": 0
"loc": 69
},
"client/src/launcher/preview.rs": {
"loc": 293,
"clippy_warnings": 20,
"doc_debt": 6
"doc_debt": 6,
"loc": 293
},
"client/src/launcher/state.rs": {
"loc": 360,
"clippy_warnings": 14,
"doc_debt": 15
"doc_debt": 15,
"loc": 360
},
"client/src/launcher/ui.rs": {
"loc": 654,
"clippy_warnings": 4,
"doc_debt": 1
"clippy_warnings": 10,
"doc_debt": 1,
"loc": 695
},
"client/src/launcher/ui_components.rs": {
"loc": 615,
"clippy_warnings": 8,
"doc_debt": 4
"doc_debt": 4,
"loc": 679
},
"client/src/launcher/ui_runtime.rs": {
"loc": 457,
"clippy_warnings": 10,
"doc_debt": 18
"doc_debt": 20,
"loc": 660
},
"client/src/layout.rs": {
"loc": 78,
"clippy_warnings": 6,
"doc_debt": 0
"doc_debt": 0,
"loc": 78
},
"client/src/lib.rs": {
"loc": 14,
"clippy_warnings": 0,
"doc_debt": 0,
"clippy_warnings": 0
"loc": 14
},
"client/src/main.rs": {
"loc": 96,
"clippy_warnings": 2,
"doc_debt": 2
"doc_debt": 2,
"loc": 96
},
"client/src/output/audio.rs": {
"loc": 195,
"clippy_warnings": 37,
"doc_debt": 5
"doc_debt": 5,
"loc": 195
},
"client/src/output/display.rs": {
"loc": 81,
"clippy_warnings": 0,
"doc_debt": 0,
"clippy_warnings": 0
"loc": 81
},
"client/src/output/layout.rs": {
"loc": 155,
"clippy_warnings": 4,
"doc_debt": 2
"doc_debt": 2,
"loc": 155
},
"client/src/output/mod.rs": {
"loc": 6,
"clippy_warnings": 0,
"doc_debt": 0,
"clippy_warnings": 0
"loc": 6
},
"client/src/output/video.rs": {
"loc": 547,
"clippy_warnings": 36,
"doc_debt": 4
"doc_debt": 4,
"loc": 547
},
"client/src/paste.rs": {
"loc": 46,
"clippy_warnings": 2,
"doc_debt": 1
"doc_debt": 1,
"loc": 46
},
"common/src/bin/cli.rs": {
"loc": 3,
"clippy_warnings": 0,
"doc_debt": 0,
"clippy_warnings": 0
"loc": 3
},
"common/src/cli.rs": {
"loc": 22,
"clippy_warnings": 0,
"doc_debt": 0,
"clippy_warnings": 0
"loc": 22
},
"common/src/hid.rs": {
"loc": 134,
"clippy_warnings": 0,
"doc_debt": 2,
"clippy_warnings": 0
"loc": 134
},
"common/src/lib.rs": {
"loc": 22,
"clippy_warnings": 0,
"doc_debt": 0,
"clippy_warnings": 0
"loc": 22
},
"common/src/paste.rs": {
"loc": 95,
"clippy_warnings": 0,
"doc_debt": 2,
"clippy_warnings": 0
"loc": 95
},
"server/src/audio.rs": {
"loc": 386,
"clippy_warnings": 37,
"doc_debt": 7
"doc_debt": 7,
"loc": 386
},
"server/src/bin/lesavka-uvc.real.inc": {
"clippy_warnings": 31,
"doc_debt": 0
"clippy_warnings": 31
},
"server/src/bin/lesavka-uvc.rs": {
"loc": 710,
"clippy_warnings": 0,
"doc_debt": 17,
"clippy_warnings": 0
"loc": 710
},
"server/src/camera.rs": {
"loc": 392,
"clippy_warnings": 12,
"doc_debt": 11
"doc_debt": 11,
"loc": 392
},
"server/src/camera_runtime.rs": {
"loc": 200,
"clippy_warnings": 10,
"doc_debt": 5
"doc_debt": 5,
"loc": 200
},
"server/src/capture_power.rs": {
"loc": 338,
"clippy_warnings": 10,
"doc_debt": 7
"doc_debt": 7,
"loc": 338
},
"server/src/gadget.rs": {
"loc": 327,
"clippy_warnings": 30,
"doc_debt": 7
"doc_debt": 7,
"loc": 327
},
"server/src/handshake.rs": {
"loc": 40,
"clippy_warnings": 2,
"doc_debt": 1
"doc_debt": 1,
"loc": 40
},
"server/src/lib.rs": {
"loc": 14,
"clippy_warnings": 0,
"doc_debt": 0,
"clippy_warnings": 0
"loc": 14
},
"server/src/main.rs": {
"loc": 572,
"clippy_warnings": 10,
"doc_debt": 13
"doc_debt": 13,
"loc": 572
},
"server/src/paste.rs": {
"loc": 207,
"clippy_warnings": 6,
"doc_debt": 3
"doc_debt": 3,
"loc": 207
},
"server/src/runtime_support.rs": {
"loc": 387,
"clippy_warnings": 14,
"doc_debt": 8
"doc_debt": 8,
"loc": 387
},
"server/src/uvc_control/model.rs": {
"loc": 510,
"clippy_warnings": 0,
"doc_debt": 11,
"clippy_warnings": 0
"loc": 510
},
"server/src/uvc_control/protocol.rs": {
"loc": 403,
"clippy_warnings": 0,
"doc_debt": 11,
"clippy_warnings": 0
"loc": 403
},
"server/src/uvc_runtime.rs": {
"loc": 241,
"clippy_warnings": 4,
"doc_debt": 5
"doc_debt": 5,
"loc": 241
},
"server/src/video.rs": {
"loc": 344,
"clippy_warnings": 25,
"doc_debt": 2
"doc_debt": 2,
"loc": 343
},
"server/src/video_sinks.rs": {
"loc": 559,
"clippy_warnings": 78,
"doc_debt": 11
"doc_debt": 11,
"loc": 559
},
"server/src/video_support.rs": {
"loc": 236,
"clippy_warnings": 8,
"doc_debt": 6
"doc_debt": 6,
"loc": 236
},
"testing/src/lib.rs": {
"loc": 10,
"clippy_warnings": 0,
"doc_debt": 0,
"clippy_warnings": 0
"loc": 10
}
}
}

View File

@ -1,168 +1,168 @@
{
"files": {
"client/src/app.rs": {
"loc": 546,
"line_percent": 95.1219512195122
"line_percent": 95.1219512195122,
"loc": 546
},
"client/src/app_support.rs": {
"loc": 128,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 128
},
"client/src/handshake.rs": {
"loc": 194,
"line_percent": 96.15384615384616
"line_percent": 96.15384615384616,
"loc": 194
},
"client/src/input/camera.rs": {
"loc": 372,
"line_percent": 98.42931937172776
"line_percent": 98.42931937172776,
"loc": 372
},
"client/src/input/inputs.rs": {
"loc": 673,
"line_percent": 97.55102040816327
"line_percent": 97.55102040816327,
"loc": 673
},
"client/src/input/keyboard.rs": {
"loc": 580,
"line_percent": 95.9409594095941
"line_percent": 95.9409594095941,
"loc": 580
},
"client/src/input/keymap.rs": {
"loc": 196,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 196
},
"client/src/input/microphone.rs": {
"loc": 166,
"line_percent": 95.94594594594594
"line_percent": 95.94594594594594,
"loc": 166
},
"client/src/input/mouse.rs": {
"loc": 317,
"line_percent": 97.32142857142857
"line_percent": 97.32142857142857,
"loc": 317
},
"client/src/launcher/clipboard.rs": {
"loc": 177,
"line_percent": 98.0
"line_percent": 98.0,
"loc": 177
},
"client/src/launcher/devices.rs": {
"loc": 158,
"line_percent": 98.13084112149532
"line_percent": 98.13084112149532,
"loc": 158
},
"client/src/launcher/diagnostics.rs": {
"loc": 175,
"line_percent": 97.14285714285714
"line_percent": 97.14285714285714,
"loc": 175
},
"client/src/launcher/mod.rs": {
"loc": 195,
"line_percent": 95.23809523809523
"line_percent": 95.23809523809523,
"loc": 195
},
"client/src/launcher/state.rs": {
"loc": 360,
"line_percent": 98.29059829059828
"line_percent": 98.29059829059828,
"loc": 360
},
"client/src/launcher/ui.rs": {
"loc": 654,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 695
},
"client/src/layout.rs": {
"loc": 78,
"line_percent": 97.72727272727273
"line_percent": 97.72727272727273,
"loc": 78
},
"client/src/main.rs": {
"loc": 96,
"line_percent": 97.0873786407767
"line_percent": 97.0873786407767,
"loc": 96
},
"client/src/output/audio.rs": {
"loc": 195,
"line_percent": 98.78048780487805
"line_percent": 98.78048780487805,
"loc": 195
},
"client/src/output/display.rs": {
"loc": 81,
"line_percent": 97.61904761904762
"line_percent": 97.61904761904762,
"loc": 81
},
"client/src/output/layout.rs": {
"loc": 155,
"line_percent": 98.9795918367347
"line_percent": 98.9795918367347,
"loc": 155
},
"client/src/output/video.rs": {
"loc": 547,
"line_percent": 96.22641509433963
"line_percent": 96.22641509433963,
"loc": 547
},
"client/src/paste.rs": {
"loc": 46,
"line_percent": 96.29629629629629
"line_percent": 96.29629629629629,
"loc": 46
},
"common/src/bin/cli.rs": {
"loc": 3,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 3
},
"common/src/cli.rs": {
"loc": 22,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 22
},
"common/src/hid.rs": {
"loc": 134,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 134
},
"common/src/lib.rs": {
"loc": 22,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 22
},
"common/src/paste.rs": {
"loc": 95,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 95
},
"server/src/audio.rs": {
"loc": 386,
"line_percent": 98.9010989010989
"line_percent": 100.0,
"loc": 386
},
"server/src/bin/lesavka-uvc.rs": {
"loc": 710,
"line_percent": 96.35535307517085
"line_percent": 96.35535307517085,
"loc": 710
},
"server/src/camera.rs": {
"loc": 392,
"line_percent": 99.09909909909909
"line_percent": 99.09909909909909,
"loc": 392
},
"server/src/camera_runtime.rs": {
"loc": 200,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 200
},
"server/src/capture_power.rs": {
"loc": 338,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 338
},
"server/src/gadget.rs": {
"loc": 327,
"line_percent": 96.875
"line_percent": 96.875,
"loc": 327
},
"server/src/handshake.rs": {
"loc": 40,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 40
},
"server/src/main.rs": {
"loc": 572,
"line_percent": 95.33333333333334
"line_percent": 95.33333333333334,
"loc": 572
},
"server/src/paste.rs": {
"loc": 207,
"line_percent": 97.12230215827337
"line_percent": 97.12230215827337,
"loc": 207
},
"server/src/runtime_support.rs": {
"loc": 387,
"line_percent": 96.42857142857143
"line_percent": 96.42857142857143,
"loc": 387
},
"server/src/uvc_runtime.rs": {
"loc": 241,
"line_percent": 97.14285714285714
"line_percent": 97.14285714285714,
"loc": 241
},
"server/src/video.rs": {
"loc": 344,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 343
},
"server/src/video_sinks.rs": {
"loc": 559,
"line_percent": 100.0
"line_percent": 100.0,
"loc": 559
},
"server/src/video_support.rs": {
"loc": 236,
"line_percent": 96.03174603174604
"line_percent": 96.03174603174604,
"loc": 236
}
}
}