ui: polish launcher logging and diagnostics
This commit is contained in:
parent
3b112996dd
commit
64ded7839d
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -2,7 +2,10 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use super::state::{CaptureSizeChoice, FeedSourcePreset, InputRouting, LauncherState, ViewMode};
|
||||
use super::{
|
||||
devices::CameraMode,
|
||||
state::{CaptureSizeChoice, FeedSourcePreset, InputRouting, LauncherState, ViewMode},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PerformanceSample {
|
||||
@ -88,6 +91,13 @@ impl DiagnosticsLog {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct MediaChannelState {
|
||||
pub camera: bool,
|
||||
pub microphone: bool,
|
||||
pub audio: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SnapshotReport {
|
||||
pub client_version: String,
|
||||
@ -138,9 +148,12 @@ pub struct SnapshotReport {
|
||||
pub right_decoded_caps_label: String,
|
||||
pub right_rendered_caps_label: String,
|
||||
pub selected_camera: Option<String>,
|
||||
pub camera_quality_label: String,
|
||||
pub selected_microphone: Option<String>,
|
||||
pub selected_speaker: Option<String>,
|
||||
pub media_channels: MediaChannelState,
|
||||
pub audio_gain_label: String,
|
||||
pub mic_gain_label: String,
|
||||
pub selected_keyboard: Option<String>,
|
||||
pub selected_mouse: Option<String>,
|
||||
pub status: String,
|
||||
@ -353,9 +366,19 @@ impl SnapshotReport {
|
||||
})
|
||||
.unwrap_or_else(|| "pending".to_string()),
|
||||
selected_camera: state.devices.camera.clone(),
|
||||
camera_quality_label: state
|
||||
.camera_quality
|
||||
.map(CameraMode::short_label)
|
||||
.unwrap_or_else(|| "default".to_string()),
|
||||
selected_microphone: state.devices.microphone.clone(),
|
||||
selected_speaker: state.devices.speaker.clone(),
|
||||
media_channels: MediaChannelState {
|
||||
camera: state.channels.camera,
|
||||
microphone: state.channels.microphone,
|
||||
audio: state.channels.audio,
|
||||
},
|
||||
audio_gain_label: state.audio_gain_label(),
|
||||
mic_gain_label: state.mic_gain_label(),
|
||||
selected_keyboard: state.devices.keyboard.clone(),
|
||||
selected_mouse: state.devices.mouse.clone(),
|
||||
status: state.status_line(),
|
||||
@ -383,16 +406,19 @@ impl SnapshotReport {
|
||||
let _ = writeln!(text, "server: {server_version} ({server_state})");
|
||||
let _ = writeln!(
|
||||
text,
|
||||
"session: routing={:?} view={:?} relay_active={} power={}",
|
||||
self.routing, self.view_mode, self.remote_active, self.power_state
|
||||
"session: routing={:?} view={:?} relay={} capture_power={}",
|
||||
self.routing,
|
||||
self.view_mode,
|
||||
if self.remote_active { "active" } else { "idle" },
|
||||
self.power_state
|
||||
);
|
||||
let _ = writeln!(
|
||||
text,
|
||||
"runtime pressure: client={:.1}% server={:.1}%",
|
||||
"runtime: client CPU {:.1}% | server CPU {:.1}%",
|
||||
self.client_process_cpu_pct, self.server_process_cpu_pct
|
||||
);
|
||||
let _ = writeln!(text, "source feed: {}", self.preview_source);
|
||||
let _ = writeln!(text, "client display limit: {}", self.client_display_limit);
|
||||
let _ = writeln!(text, "display limit: {}", self.client_display_limit);
|
||||
let _ = writeln!(text);
|
||||
let _ = writeln!(text, "left eye");
|
||||
let _ = writeln!(text, " surface: {}", self.left_surface);
|
||||
@ -451,23 +477,28 @@ impl SnapshotReport {
|
||||
self.right_server_queue_peak
|
||||
);
|
||||
let _ = writeln!(text);
|
||||
let _ = writeln!(text, "device staging");
|
||||
let _ = writeln!(text, "media staging");
|
||||
let _ = writeln!(
|
||||
text,
|
||||
" camera: {}",
|
||||
self.selected_camera.as_deref().unwrap_or("auto")
|
||||
" camera: {} | quality={} | enabled={}",
|
||||
self.selected_camera.as_deref().unwrap_or("auto"),
|
||||
self.camera_quality_label,
|
||||
self.media_channels.camera
|
||||
);
|
||||
let _ = writeln!(
|
||||
text,
|
||||
" microphone: {}",
|
||||
self.selected_microphone.as_deref().unwrap_or("auto")
|
||||
" speaker: {} | volume={} | enabled={}",
|
||||
self.selected_speaker.as_deref().unwrap_or("auto"),
|
||||
self.audio_gain_label,
|
||||
self.media_channels.audio
|
||||
);
|
||||
let _ = writeln!(
|
||||
text,
|
||||
" speaker: {}",
|
||||
self.selected_speaker.as_deref().unwrap_or("auto")
|
||||
" microphone: {} | gain={} | enabled={}",
|
||||
self.selected_microphone.as_deref().unwrap_or("auto"),
|
||||
self.mic_gain_label,
|
||||
self.media_channels.microphone
|
||||
);
|
||||
let _ = writeln!(text, " audio gain: {}", self.audio_gain_label);
|
||||
let _ = writeln!(
|
||||
text,
|
||||
" keyboard: {}",
|
||||
@ -479,7 +510,7 @@ impl SnapshotReport {
|
||||
self.selected_mouse.as_deref().unwrap_or("all")
|
||||
);
|
||||
let _ = writeln!(text);
|
||||
let _ = writeln!(text, "launcher status");
|
||||
let _ = writeln!(text, "current UI state");
|
||||
let _ = writeln!(text, " {}", self.status);
|
||||
let _ = writeln!(text);
|
||||
let _ = writeln!(text, "recent samples");
|
||||
@ -931,9 +962,38 @@ mod tests {
|
||||
assert!(text.contains("stream caps:"));
|
||||
assert!(text.contains("decoded caps:"));
|
||||
assert!(text.contains("rendered caps:"));
|
||||
assert!(text.contains("media staging"));
|
||||
assert!(text.contains("current UI state"));
|
||||
assert!(text.contains("recommendations"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[doc = "Verifies diagnostics text follows live media settings."]
|
||||
fn snapshot_text_reflects_live_media_control_changes() {
|
||||
let mut state = LauncherState::new();
|
||||
state.select_camera(Some("/dev/video9".to_string()));
|
||||
state.select_camera_quality(Some(crate::launcher::devices::CameraMode::new(
|
||||
1920, 1080, 30,
|
||||
)));
|
||||
state.select_microphone(Some("alsa_input.usb".to_string()));
|
||||
state.select_speaker(Some("alsa_output.usb".to_string()));
|
||||
state.set_audio_gain_percent(250);
|
||||
state.set_mic_gain_percent(125);
|
||||
state.set_camera_channel_enabled(false);
|
||||
state.set_microphone_channel_enabled(true);
|
||||
|
||||
let report = SnapshotReport::from_state(
|
||||
&state,
|
||||
&DiagnosticsLog::new(1),
|
||||
quality_probe_command().to_string(),
|
||||
);
|
||||
let text = report.to_pretty_text();
|
||||
|
||||
assert!(text.contains("camera: /dev/video9 | quality=1080p@30 | enabled=false"));
|
||||
assert!(text.contains("speaker: alsa_output.usb | volume=250% | enabled=true"));
|
||||
assert!(text.contains("microphone: alsa_input.usb | gain=125% | enabled=true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_report_uses_effective_mirrored_capture_profile() {
|
||||
let mut state = LauncherState::new();
|
||||
|
||||
@ -15,21 +15,21 @@ use {
|
||||
MAX_MIC_GAIN_PERCENT,
|
||||
},
|
||||
super::ui_components::{
|
||||
build_launcher_view, sync_camera_quality_combo, sync_input_device_combo,
|
||||
ConsoleLogLevel, build_launcher_view, sync_camera_quality_combo, sync_input_device_combo,
|
||||
sync_stage_device_combo,
|
||||
},
|
||||
super::ui_runtime::{
|
||||
RelayChild, append_session_log, apply_popout_window_size, attach_child_log_streams,
|
||||
audio_gain_control_path, capture_swap_key, copy_plain_text, copy_session_log,
|
||||
dock_all_displays_to_preview, dock_display_to_preview, input_control_path,
|
||||
input_state_path, input_toggle_control_path, mic_gain_control_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, uplink_camera_preview_path, uplink_mic_level_path,
|
||||
write_audio_gain_request, write_input_routing_request, write_input_toggle_key_request,
|
||||
write_mic_gain_request,
|
||||
RelayChild, append_session_log_for_level, apply_popout_window_size,
|
||||
attach_child_log_streams, audio_gain_control_path, capture_swap_key, copy_plain_text,
|
||||
copy_session_log, dock_all_displays_to_preview, dock_display_to_preview,
|
||||
input_control_path, input_state_path, input_toggle_control_path, mic_gain_control_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, uplink_camera_preview_path,
|
||||
uplink_mic_level_path, write_audio_gain_request, write_input_routing_request,
|
||||
write_input_toggle_key_request, write_mic_gain_request,
|
||||
},
|
||||
crate::handshake::{HandshakeProbe, probe},
|
||||
crate::output::display::enumerate_monitors,
|
||||
@ -1790,6 +1790,22 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let widgets = widgets.clone();
|
||||
widgets.console_level_combo.connect_changed(move |combo| {
|
||||
let level = combo
|
||||
.active_id()
|
||||
.as_deref()
|
||||
.and_then(ConsoleLogLevel::from_id)
|
||||
.unwrap_or_default();
|
||||
*widgets.session_log_level.borrow_mut() = level;
|
||||
widgets.status_label.set_text(&format!(
|
||||
"Console now shows {} relay logs and higher.",
|
||||
level.label()
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let widgets = widgets.clone();
|
||||
widgets.console_copy_button.connect_clicked(move |_| {
|
||||
@ -2212,16 +2228,13 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
||||
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
|
||||
"Relay connected with inputs routed to {routing}, but capture is forced off. Return capture to Auto or Force On when you want remote video."
|
||||
),
|
||||
"forced-on" => format!(
|
||||
"Relay connected with inputs routed to {}. Capture is being held awake and the eye previews are coming online.",
|
||||
routing
|
||||
"Relay connected with inputs routed to {routing}. Capture is being held awake and the eye previews are coming online."
|
||||
),
|
||||
_ => format!(
|
||||
"Relay connected with inputs routed to {}. The eye previews will come up with the live session.",
|
||||
routing
|
||||
"Relay connected with inputs routed to {routing}. The eye previews will come up with the live session."
|
||||
),
|
||||
};
|
||||
widgets.status_label.set_text(&message);
|
||||
@ -2252,12 +2265,15 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
||||
}
|
||||
|
||||
while let Ok(line) = log_rx.try_recv() {
|
||||
append_session_log(&widgets.session_log_buffer, &line);
|
||||
let level = *widgets.session_log_level.borrow();
|
||||
if append_session_log_for_level(&widgets.session_log_buffer, &line, level)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
while let Ok(message) = power_rx.try_recv() {
|
||||
power_request_in_flight.set(false);
|
||||
|
||||
@ -48,6 +48,76 @@ pub struct PopoutWindowHandle {
|
||||
pub binding: PreviewBinding,
|
||||
}
|
||||
|
||||
/// Minimum severity for relay log lines shown in the session console.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum ConsoleLogLevel {
|
||||
Error,
|
||||
#[default]
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
impl ConsoleLogLevel {
|
||||
pub const ALL: [Self; 5] = [
|
||||
Self::Error,
|
||||
Self::Warn,
|
||||
Self::Info,
|
||||
Self::Debug,
|
||||
Self::Trace,
|
||||
];
|
||||
|
||||
/// Stable value stored on the GTK combo row.
|
||||
#[must_use]
|
||||
pub const fn id(self) -> &'static str {
|
||||
match self {
|
||||
Self::Error => "error",
|
||||
Self::Warn => "warn",
|
||||
Self::Info => "info",
|
||||
Self::Debug => "debug",
|
||||
Self::Trace => "trace",
|
||||
}
|
||||
}
|
||||
|
||||
/// Short label displayed in the launcher dropdown.
|
||||
#[must_use]
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Error => "Error",
|
||||
Self::Warn => "Warn",
|
||||
Self::Info => "Info",
|
||||
Self::Debug => "Debug",
|
||||
Self::Trace => "Trace",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[doc = "Parses a GTK combo id back into a log level."]
|
||||
pub fn from_id(raw: &str) -> Option<Self> {
|
||||
match raw {
|
||||
"error" => Some(Self::Error),
|
||||
"warn" => Some(Self::Warn),
|
||||
"info" => Some(Self::Info),
|
||||
"debug" => Some(Self::Debug),
|
||||
"trace" => Some(Self::Trace),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Numeric ordering where lower values are more important.
|
||||
#[must_use]
|
||||
pub const fn rank(self) -> u8 {
|
||||
match self {
|
||||
Self::Error => 0,
|
||||
Self::Warn => 1,
|
||||
Self::Info => 2,
|
||||
Self::Debug => 3,
|
||||
Self::Trace => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LauncherWidgets {
|
||||
pub status_label: gtk::Label,
|
||||
@ -96,6 +166,8 @@ pub struct LauncherWidgets {
|
||||
pub diagnostics_popout_button: gtk::Button,
|
||||
pub console_copy_button: gtk::Button,
|
||||
pub console_popout_button: gtk::Button,
|
||||
pub console_level_combo: gtk::ComboBoxText,
|
||||
pub session_log_level: Rc<RefCell<ConsoleLogLevel>>,
|
||||
pub _device_body_height_group: gtk::SizeGroup,
|
||||
}
|
||||
|
||||
@ -161,15 +233,23 @@ pub fn build_launcher_view(
|
||||
hero.set_hexpand(true);
|
||||
|
||||
let brand_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
brand_box.set_valign(gtk::Align::Center);
|
||||
let brand_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
brand_row.set_halign(gtk::Align::Start);
|
||||
brand_row.set_valign(gtk::Align::Center);
|
||||
let brand_icon = gtk::Image::from_icon_name(LESAVKA_ICON_NAME);
|
||||
brand_icon.add_css_class("app-logo");
|
||||
brand_icon.set_pixel_size(28);
|
||||
brand_icon.set_valign(gtk::Align::Center);
|
||||
let heading = gtk::Label::new(Some("Lesavka"));
|
||||
heading.add_css_class("title-2");
|
||||
heading.set_halign(gtk::Align::Start);
|
||||
heading.set_valign(gtk::Align::Center);
|
||||
let version_tag = gtk::Label::new(Some(&format!("v{}", crate::VERSION)));
|
||||
version_tag.add_css_class("version-tag");
|
||||
version_tag.set_halign(gtk::Align::Start);
|
||||
version_tag.set_valign(gtk::Align::End);
|
||||
version_tag.set_valign(gtk::Align::Center);
|
||||
brand_row.append(&brand_icon);
|
||||
brand_row.append(&heading);
|
||||
brand_row.append(&version_tag);
|
||||
brand_box.append(&brand_row);
|
||||
@ -226,9 +306,7 @@ pub fn build_launcher_view(
|
||||
|
||||
let device_refresh_button = gtk::Button::with_label("Refresh Devices");
|
||||
stabilize_button(&device_refresh_button, 132);
|
||||
device_refresh_button.set_tooltip_text(Some(
|
||||
"Re-scan webcams, microphones, speakers, keyboards, and mice without restarting Lesavka.",
|
||||
));
|
||||
device_refresh_button.set_tooltip_text(Some("Re-scan connected devices."));
|
||||
let (devices_panel, devices_body) =
|
||||
build_panel_with_action("Device Staging", Some(device_refresh_button.upcast_ref()));
|
||||
devices_panel.set_hexpand(true);
|
||||
@ -253,14 +331,10 @@ pub fn build_launcher_view(
|
||||
state.selected_camera_quality(catalog),
|
||||
);
|
||||
camera_quality_combo.set_size_request(88, -1);
|
||||
camera_quality_combo.set_tooltip_text(Some(
|
||||
"Choose the webcam quality Lesavka should capture and send to the relay host.",
|
||||
));
|
||||
camera_quality_combo.set_tooltip_text(Some("Webcam uplink quality."));
|
||||
let camera_test_button = gtk::Button::with_label("Start Preview");
|
||||
stabilize_button(&camera_test_button, 118);
|
||||
camera_test_button.set_tooltip_text(Some(
|
||||
"Open a local preview for the selected webcam so you can confirm the right source.",
|
||||
));
|
||||
camera_test_button.set_tooltip_text(Some("Preview selected webcam locally."));
|
||||
|
||||
let speaker_combo = gtk::ComboBoxText::new();
|
||||
sync_stage_device_combo(
|
||||
@ -270,9 +344,7 @@ pub fn build_launcher_view(
|
||||
);
|
||||
let speaker_test_button = gtk::Button::with_label("Play Tone");
|
||||
stabilize_button(&speaker_test_button, 118);
|
||||
speaker_test_button.set_tooltip_text(Some(
|
||||
"Play a short continuous tone through the selected speaker until you stop the test.",
|
||||
));
|
||||
speaker_test_button.set_tooltip_text(Some("Play a local test tone."));
|
||||
|
||||
let keyboard_combo = gtk::ComboBoxText::new();
|
||||
keyboard_combo.append(Some("all"), "all keyboards");
|
||||
@ -280,9 +352,7 @@ pub fn build_launcher_view(
|
||||
append_input_choice(&keyboard_combo, keyboard);
|
||||
}
|
||||
super::ui_runtime::set_combo_active_text(&keyboard_combo, state.devices.keyboard.as_deref());
|
||||
keyboard_combo.set_tooltip_text(Some(
|
||||
"Leave this on all keyboards to relay every keyboard, or pick one specific device.",
|
||||
));
|
||||
keyboard_combo.set_tooltip_text(Some("Keyboard source for relay input."));
|
||||
let keyboard_row = build_inline_selector_row("Keyboard", &keyboard_combo);
|
||||
control_stack.append(&keyboard_row);
|
||||
|
||||
@ -292,9 +362,7 @@ pub fn build_launcher_view(
|
||||
append_input_choice(&mouse_combo, mouse);
|
||||
}
|
||||
super::ui_runtime::set_combo_active_text(&mouse_combo, state.devices.mouse.as_deref());
|
||||
mouse_combo.set_tooltip_text(Some(
|
||||
"Leave this on all mice to relay every pointer, or pick one specific device.",
|
||||
));
|
||||
mouse_combo.set_tooltip_text(Some("Pointer source for relay input."));
|
||||
let mouse_row = build_inline_selector_row("Mouse", &mouse_combo);
|
||||
control_stack.append(&mouse_row);
|
||||
devices_body.append(&control_group);
|
||||
@ -306,24 +374,18 @@ pub fn build_launcher_view(
|
||||
media_group.append(&media_grid);
|
||||
let camera_channel_toggle = gtk::CheckButton::with_label("Camera");
|
||||
camera_channel_toggle.set_active(state.channels.camera);
|
||||
camera_channel_toggle.set_tooltip_text(Some(
|
||||
"Include the local webcam uplink in the next relay session.",
|
||||
));
|
||||
camera_channel_toggle.set_tooltip_text(Some("Send webcam during relay."));
|
||||
let audio_channel_toggle = gtk::CheckButton::with_label("Speaker");
|
||||
audio_channel_toggle.set_active(state.channels.audio);
|
||||
audio_channel_toggle.set_tooltip_text(Some(
|
||||
"Play remote audio on this client during the next relay session.",
|
||||
));
|
||||
audio_channel_toggle.set_tooltip_text(Some("Play remote audio here."));
|
||||
let microphone_channel_toggle = gtk::CheckButton::with_label("Mic");
|
||||
microphone_channel_toggle.set_active(state.channels.microphone);
|
||||
microphone_channel_toggle.set_tooltip_text(Some(
|
||||
"Include the local microphone uplink in the next relay session.",
|
||||
));
|
||||
microphone_channel_toggle.set_tooltip_text(Some("Send mic during relay."));
|
||||
|
||||
let audio_gain_adjustment = gtk::Adjustment::new(
|
||||
state.audio_gain_percent as f64,
|
||||
f64::from(state.audio_gain_percent),
|
||||
0.0,
|
||||
super::state::MAX_AUDIO_GAIN_PERCENT as f64,
|
||||
f64::from(super::state::MAX_AUDIO_GAIN_PERCENT),
|
||||
25.0,
|
||||
100.0,
|
||||
0.0,
|
||||
@ -333,16 +395,18 @@ pub fn build_launcher_view(
|
||||
audio_gain_scale.set_draw_value(false);
|
||||
audio_gain_scale.set_hexpand(false);
|
||||
audio_gain_scale.set_size_request(96, -1);
|
||||
audio_gain_scale.set_tooltip_text(Some(
|
||||
"Boost or lower remote speaker playback on this client. Changes apply live while the relay is connected.",
|
||||
));
|
||||
audio_gain_scale.set_tooltip_text(Some("Speaker volume. Double-click resets to 200%."));
|
||||
attach_scale_reset_gesture(
|
||||
&audio_gain_scale,
|
||||
f64::from(super::state::DEFAULT_AUDIO_GAIN_PERCENT),
|
||||
);
|
||||
let audio_gain_value = gtk::Label::new(Some(&state.audio_gain_label()));
|
||||
audio_gain_value.set_visible(false);
|
||||
|
||||
let mic_gain_adjustment = gtk::Adjustment::new(
|
||||
state.mic_gain_percent as f64,
|
||||
f64::from(state.mic_gain_percent),
|
||||
0.0,
|
||||
super::state::MAX_MIC_GAIN_PERCENT as f64,
|
||||
f64::from(super::state::MAX_MIC_GAIN_PERCENT),
|
||||
25.0,
|
||||
100.0,
|
||||
0.0,
|
||||
@ -351,9 +415,11 @@ pub fn build_launcher_view(
|
||||
mic_gain_scale.set_draw_value(false);
|
||||
mic_gain_scale.set_hexpand(false);
|
||||
mic_gain_scale.set_size_request(96, -1);
|
||||
mic_gain_scale.set_tooltip_text(Some(
|
||||
"Boost or lower local microphone uplink gain. Changes apply live while microphone uplink is running.",
|
||||
));
|
||||
mic_gain_scale.set_tooltip_text(Some("Mic gain. Double-click resets to 100%."));
|
||||
attach_scale_reset_gesture(
|
||||
&mic_gain_scale,
|
||||
f64::from(super::state::DEFAULT_MIC_GAIN_PERCENT),
|
||||
);
|
||||
let mic_gain_value = gtk::Label::new(Some(&state.mic_gain_label()));
|
||||
mic_gain_value.set_visible(false);
|
||||
|
||||
@ -391,9 +457,7 @@ pub fn build_launcher_view(
|
||||
);
|
||||
let microphone_test_button = gtk::Button::with_label("Monitor Mic");
|
||||
stabilize_button(µphone_test_button, 118);
|
||||
microphone_test_button.set_tooltip_text(Some(
|
||||
"Monitor the selected microphone through the selected speaker until you stop the test.",
|
||||
));
|
||||
microphone_test_button.set_tooltip_text(Some("Monitor mic through speaker."));
|
||||
microphone_combo.set_size_request(0, -1);
|
||||
let microphone_selectors = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||
microphone_combo.set_hexpand(true);
|
||||
@ -509,9 +573,7 @@ pub fn build_launcher_view(
|
||||
server_entry.set_hexpand(true);
|
||||
server_entry.set_width_chars(18);
|
||||
server_entry.set_text(server_addr);
|
||||
server_entry.set_tooltip_text(Some(
|
||||
"Relay host address for previews, power control, and the live session.",
|
||||
));
|
||||
server_entry.set_tooltip_text(Some("Relay host address."));
|
||||
let relay_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
relay_row.set_halign(gtk::Align::Fill);
|
||||
relay_row.set_hexpand(true);
|
||||
@ -528,21 +590,15 @@ pub fn build_launcher_view(
|
||||
let clipboard_button = gtk::Button::with_label("Send Clipboard");
|
||||
clipboard_button.set_hexpand(true);
|
||||
stabilize_button(&clipboard_button, 108);
|
||||
clipboard_button.set_tooltip_text(Some(
|
||||
"Type the current local clipboard into the remote target. This stays launcher-only.",
|
||||
));
|
||||
clipboard_button.set_tooltip_text(Some("Type clipboard remotely."));
|
||||
let probe_button = gtk::Button::with_label("Copy Gate Probe");
|
||||
probe_button.set_hexpand(true);
|
||||
stabilize_button(&probe_button, 108);
|
||||
probe_button.set_tooltip_text(Some(
|
||||
"Copy the hygiene/quality probe command into the local clipboard.",
|
||||
));
|
||||
probe_button.set_tooltip_text(Some("Copy quality probe."));
|
||||
let usb_recover_button = gtk::Button::with_label("Recover USB");
|
||||
usb_recover_button.set_hexpand(true);
|
||||
stabilize_button(&usb_recover_button, 108);
|
||||
usb_recover_button.set_tooltip_text(Some(
|
||||
"Force the remote USB gadget to re-enumerate when keyboard, mouse, webcam, or audio stop showing up on the host.",
|
||||
));
|
||||
usb_recover_button.set_tooltip_text(Some("Re-enumerate remote USB."));
|
||||
live_actions_row.append(&clipboard_button);
|
||||
live_actions_row.append(&probe_button);
|
||||
live_actions_row.append(&usb_recover_button);
|
||||
@ -599,9 +655,7 @@ pub fn build_launcher_view(
|
||||
let input_toggle_button = gtk::Button::with_label("Route");
|
||||
input_toggle_button.set_hexpand(true);
|
||||
stabilize_button(&input_toggle_button, 106);
|
||||
input_toggle_button.set_tooltip_text(Some(
|
||||
"Change live keyboard and mouse ownership between this machine and the remote target.",
|
||||
));
|
||||
input_toggle_button.set_tooltip_text(Some("Swap input ownership."));
|
||||
let swap_key_button = gtk::Button::with_label("Set Swap Key");
|
||||
swap_key_button.set_hexpand(true);
|
||||
stabilize_button(&swap_key_button, 106);
|
||||
@ -652,11 +706,21 @@ pub fn build_launcher_view(
|
||||
console_panel.set_valign(gtk::Align::Fill);
|
||||
console_body.set_vexpand(true);
|
||||
let console_toolbar = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
console_toolbar.set_homogeneous(true);
|
||||
let console_copy_button = gtk::Button::with_label("Copy Log");
|
||||
stabilize_button(&console_copy_button, 104);
|
||||
let console_popout_button = gtk::Button::with_label("Break Out Log");
|
||||
stabilize_button(&console_popout_button, 104);
|
||||
let session_log_level = Rc::new(RefCell::new(ConsoleLogLevel::default()));
|
||||
let console_level_combo = gtk::ComboBoxText::new();
|
||||
for level in ConsoleLogLevel::ALL {
|
||||
console_level_combo.append(Some(level.id()), level.label());
|
||||
}
|
||||
console_level_combo.set_active_id(Some(ConsoleLogLevel::default().id()));
|
||||
console_level_combo.set_size_request(78, 36);
|
||||
console_level_combo.set_tooltip_text(Some("Show relay logs at this level or higher."));
|
||||
let console_copy_button = gtk::Button::with_label("Copy");
|
||||
stabilize_button(&console_copy_button, 66);
|
||||
console_copy_button.set_tooltip_text(Some("Copy visible log."));
|
||||
let console_popout_button = gtk::Button::with_label("Pop Out");
|
||||
stabilize_button(&console_popout_button, 78);
|
||||
console_popout_button.set_tooltip_text(Some("Open log window."));
|
||||
console_toolbar.append(&console_level_combo);
|
||||
console_toolbar.append(&console_copy_button);
|
||||
console_toolbar.append(&console_popout_button);
|
||||
let status_label = gtk::Label::new(Some("Session log ready."));
|
||||
@ -853,6 +917,8 @@ pub fn build_launcher_view(
|
||||
diagnostics_popout_button: diagnostics_popout_button.clone(),
|
||||
console_copy_button: console_copy_button.clone(),
|
||||
console_popout_button: console_popout_button.clone(),
|
||||
console_level_combo: console_level_combo.clone(),
|
||||
session_log_level: session_log_level.clone(),
|
||||
_device_body_height_group: device_body_height_group,
|
||||
};
|
||||
let popouts = Rc::new(RefCell::new([None, None]));
|
||||
@ -887,7 +953,7 @@ pub fn build_launcher_view(
|
||||
pub fn install_css(window: >k::ApplicationWindow) {
|
||||
let provider = gtk::CssProvider::new();
|
||||
provider.load_from_data(
|
||||
r#"
|
||||
r"
|
||||
window.lesavka {
|
||||
background: #101319;
|
||||
color: #eef2f7;
|
||||
@ -919,7 +985,9 @@ pub fn install_css(window: >k::ApplicationWindow) {
|
||||
label.version-tag {
|
||||
font-size: 0.76rem;
|
||||
opacity: 0.72;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
image.app-logo {
|
||||
opacity: 0.96;
|
||||
}
|
||||
box.status-chip {
|
||||
background: rgba(91, 179, 162, 0.12);
|
||||
@ -975,7 +1043,12 @@ pub fn install_css(window: >k::ApplicationWindow) {
|
||||
}
|
||||
label.eye-inline-status {
|
||||
font-size: 0.86rem;
|
||||
opacity: 0.82;
|
||||
font-weight: 600;
|
||||
background: rgba(91, 179, 162, 0.10);
|
||||
border: 1px solid rgba(91, 179, 162, 0.22);
|
||||
border-radius: 999px;
|
||||
padding: 5px 8px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
textview.status-log,
|
||||
label.status-log {
|
||||
@ -1009,7 +1082,7 @@ pub fn install_css(window: >k::ApplicationWindow) {
|
||||
border-color: rgba(91, 179, 162, 0.45);
|
||||
font-weight: 700;
|
||||
}
|
||||
"#,
|
||||
",
|
||||
);
|
||||
if let Some(display) = gtk::gdk::Display::default() {
|
||||
gtk::style_context_add_provider_for_display(
|
||||
@ -1444,27 +1517,21 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
||||
root.append(&stack);
|
||||
|
||||
let feed_source_combo = gtk::ComboBoxText::new();
|
||||
feed_source_combo.set_tooltip_text(Some(
|
||||
"Choose which physical eye feed appears in this pane. Off disables the pane; the opposite-eye option mirrors the other physical feed while preserving a separate stream load for realistic validation.",
|
||||
));
|
||||
feed_source_combo.set_tooltip_text(Some("Eye source for this pane."));
|
||||
feed_source_combo.set_hexpand(true);
|
||||
feed_source_combo.set_size_request(0, -1);
|
||||
let capture_resolution_combo = gtk::ComboBoxText::new();
|
||||
capture_resolution_combo.set_tooltip_text(Some(
|
||||
"Choose the eye-stream source mode for this feed. Source keeps the HDMI device's own H.264 stream; cheaper source-device modes will appear here once the hardware proves it supports them.",
|
||||
));
|
||||
capture_resolution_combo.set_tooltip_text(Some("Eye capture mode."));
|
||||
capture_resolution_combo.set_size_request(0, -1);
|
||||
capture_resolution_combo.set_hexpand(true);
|
||||
let breakout_combo = gtk::ComboBoxText::new();
|
||||
breakout_combo.set_tooltip_text(Some(
|
||||
"Choose the client-side breakout window size for this eye feed. Source Size preserves the feed's own dimensions; Display Size fills the effective monitor size.",
|
||||
));
|
||||
breakout_combo.set_tooltip_text(Some("Breakout window size."));
|
||||
breakout_combo.set_size_request(0, -1);
|
||||
breakout_combo.set_hexpand(true);
|
||||
let action_button = gtk::Button::with_label("Break Out");
|
||||
stabilize_button(&action_button, 104);
|
||||
action_button.set_halign(gtk::Align::End);
|
||||
let stream_status = gtk::Label::new(Some("Connect relay to preview."));
|
||||
let stream_status = gtk::Label::new(Some("Preview pending"));
|
||||
stream_status.add_css_class("status-line");
|
||||
stream_status.add_css_class("eye-inline-status");
|
||||
stream_status.set_halign(gtk::Align::Fill);
|
||||
@ -1474,7 +1541,7 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
||||
stream_status.set_single_line_mode(true);
|
||||
stream_status.set_width_chars(12);
|
||||
stream_status.set_max_width_chars(18);
|
||||
stream_status.set_tooltip_text(Some("Connect relay to preview."));
|
||||
stream_status.set_tooltip_text(Some("Eye stream status."));
|
||||
let footer_shell = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||
let controls_grid = gtk::Grid::new();
|
||||
controls_grid.set_column_spacing(8);
|
||||
@ -1513,3 +1580,16 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
||||
fn stabilize_button(button: >k::Button, width: i32) {
|
||||
button.set_size_request(width, 36);
|
||||
}
|
||||
|
||||
/// Resets one slider to its default value on double-click.
|
||||
fn attach_scale_reset_gesture(scale: >k::Scale, default_value: f64) {
|
||||
let gesture = gtk::GestureClick::new();
|
||||
gesture.set_button(0);
|
||||
let scale_for_click = scale.clone();
|
||||
gesture.connect_released(move |_, n_press, _, _| {
|
||||
if n_press == 2 && (scale_for_click.value() - default_value).abs() > f64::EPSILON {
|
||||
scale_for_click.set_value(default_value);
|
||||
}
|
||||
});
|
||||
scale.add_controller(gesture);
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ use super::{
|
||||
preview::{LauncherPreview, PreviewSurface},
|
||||
runtime_env_vars,
|
||||
state::{BreakoutSizeChoice, CapturePowerStatus, DisplaySurface, InputRouting, LauncherState},
|
||||
ui_components::{DisplayPaneWidgets, LauncherWidgets, PopoutWindowHandle},
|
||||
ui_components::{ConsoleLogLevel, DisplayPaneWidgets, LauncherWidgets, PopoutWindowHandle},
|
||||
};
|
||||
|
||||
pub const INPUT_CONTROL_ENV: &str = "LESAVKA_LAUNCHER_INPUT_CONTROL";
|
||||
@ -117,9 +117,9 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
||||
widgets.start_button.set_sensitive(true);
|
||||
widgets.server_entry.set_sensitive(!relay_live);
|
||||
widgets.start_button.set_tooltip_text(Some(if relay_live {
|
||||
"Disconnect from the relay host, stop the live session, and let capture fall back to grace/standby."
|
||||
"Disconnect the relay session."
|
||||
} else {
|
||||
"Connect to the relay host, start the live session, and bring the eye previews online."
|
||||
"Start relay and previews."
|
||||
}));
|
||||
widgets.clipboard_button.set_sensitive(relay_live);
|
||||
widgets.probe_button.set_sensitive(true);
|
||||
@ -169,18 +169,16 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
||||
widgets
|
||||
.input_toggle_button
|
||||
.set_tooltip_text(Some(match state.routing {
|
||||
InputRouting::Remote => {
|
||||
"Inputs are currently going to the remote session. Click to bring them back local."
|
||||
}
|
||||
InputRouting::Local => {
|
||||
"Inputs are currently staying local. Click to hand them to the remote session."
|
||||
}
|
||||
InputRouting::Remote => "Route inputs back local.",
|
||||
InputRouting::Local => "Route inputs to remote.",
|
||||
}));
|
||||
widgets.swap_key_button.set_label("Set Swap Key");
|
||||
widgets.swap_key_button.set_tooltip_text(Some(if state.swap_key_binding {
|
||||
"Waiting for the next key press. The top chip still shows the current live shortcut."
|
||||
widgets
|
||||
.swap_key_button
|
||||
.set_tooltip_text(Some(if state.swap_key_binding {
|
||||
"Press the new swap key."
|
||||
} else {
|
||||
"Capture the next key you press and make it the swap shortcut. The current shortcut is shown in the top chip."
|
||||
"Set the swap shortcut."
|
||||
}));
|
||||
let power_available = state.capture_power.available;
|
||||
widgets
|
||||
@ -1089,6 +1087,33 @@ pub fn append_session_log(buffer: >k::TextBuffer, message: &str) {
|
||||
if trimmed.is_empty() {
|
||||
return;
|
||||
}
|
||||
append_clean_session_log(buffer, trimmed);
|
||||
}
|
||||
|
||||
/// Appends a session log line only when it passes the selected severity filter.
|
||||
pub fn append_session_log_for_level(
|
||||
buffer: >k::TextBuffer,
|
||||
message: &str,
|
||||
level: ConsoleLogLevel,
|
||||
) -> bool {
|
||||
let cleaned = strip_ansi_sequences(message);
|
||||
let trimmed = cleaned.trim();
|
||||
if trimmed.is_empty() || !should_show_clean_session_log_line(trimmed, level) {
|
||||
return false;
|
||||
}
|
||||
append_clean_session_log(buffer, trimmed);
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn should_show_session_log_line(message: &str, level: ConsoleLogLevel) -> bool {
|
||||
let cleaned = strip_ansi_sequences(message);
|
||||
let trimmed = cleaned.trim();
|
||||
!trimmed.is_empty() && should_show_clean_session_log_line(trimmed, level)
|
||||
}
|
||||
|
||||
/// Writes a cleaned line with the GTK text tags that match its source/severity.
|
||||
fn append_clean_session_log(buffer: >k::TextBuffer, trimmed: &str) {
|
||||
let mut end = buffer.end_iter();
|
||||
let tags = classify_log_tags(trimmed);
|
||||
if tags.is_empty() {
|
||||
@ -1202,7 +1227,7 @@ pub fn open_session_log_popout(
|
||||
None,
|
||||
buffer,
|
||||
"Lesavka Log",
|
||||
"Copy Log",
|
||||
"Copy",
|
||||
gtk::WrapMode::WordChar,
|
||||
);
|
||||
}
|
||||
@ -1454,6 +1479,71 @@ fn set_status_light(light: >k::Box, state: StatusLightState) {
|
||||
light.add_css_class(state.css_class());
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum SessionLogSeverity {
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl SessionLogSeverity {
|
||||
const fn rank(self) -> Option<u8> {
|
||||
match self {
|
||||
Self::Error => Some(0),
|
||||
Self::Warn => Some(1),
|
||||
Self::Info => Some(2),
|
||||
Self::Debug => Some(3),
|
||||
Self::Trace => Some(4),
|
||||
Self::Other => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether a cleaned log line should be visible at the selected threshold.
|
||||
fn should_show_clean_session_log_line(message: &str, level: ConsoleLogLevel) -> bool {
|
||||
let severity = session_log_severity(message);
|
||||
if severity == SessionLogSeverity::Error {
|
||||
return true;
|
||||
}
|
||||
if message.starts_with("[launcher]") || message.starts_with("[preview:") {
|
||||
return true;
|
||||
}
|
||||
match severity.rank() {
|
||||
Some(rank) => rank <= level.rank(),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Infers severity from the structured relay prefix and human-readable text.
|
||||
fn session_log_severity(message: &str) -> SessionLogSeverity {
|
||||
let uppercase = message.to_ascii_uppercase();
|
||||
if message.starts_with("[relay:stderr]")
|
||||
|| message.contains('❌')
|
||||
|| uppercase.contains(" ERROR ")
|
||||
|| uppercase.contains("FAILED")
|
||||
|| uppercase.contains("PANIC")
|
||||
|| uppercase.contains(" RPC FAILED")
|
||||
{
|
||||
SessionLogSeverity::Error
|
||||
} else if uppercase.contains(" WARN ")
|
||||
|| uppercase.contains("UNAVAILABLE")
|
||||
|| uppercase.contains("WAITING FOR CAPTURE PIPELINE")
|
||||
{
|
||||
SessionLogSeverity::Warn
|
||||
} else if uppercase.contains(" INFO ") {
|
||||
SessionLogSeverity::Info
|
||||
} else if uppercase.contains(" DEBUG ") {
|
||||
SessionLogSeverity::Debug
|
||||
} else if uppercase.contains(" TRACE ") {
|
||||
SessionLogSeverity::Trace
|
||||
} else {
|
||||
SessionLogSeverity::Other
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_log_tags(message: &str) -> Vec<&'static str> {
|
||||
let mut tags = Vec::new();
|
||||
if message.starts_with("[launcher]") {
|
||||
@ -1469,7 +1559,9 @@ fn classify_log_tags(message: &str) -> Vec<&'static str> {
|
||||
}
|
||||
|
||||
let uppercase = message.to_ascii_uppercase();
|
||||
if uppercase.contains(" ERROR ")
|
||||
if message.starts_with("[relay:stderr]")
|
||||
|| message.contains('❌')
|
||||
|| uppercase.contains(" ERROR ")
|
||||
|| uppercase.contains("FAILED")
|
||||
|| uppercase.contains("PANIC")
|
||||
|| uppercase.contains(" RPC FAILED")
|
||||
@ -1585,6 +1677,35 @@ mod tests {
|
||||
assert!(tags.contains(&"log-error") || tags.contains(&"log-warn"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[doc = "Verifies the default console filter hides relay INFO noise."]
|
||||
fn session_log_filter_hides_noisy_info_by_default_but_keeps_errors() {
|
||||
assert!(!should_show_session_log_line(
|
||||
"[relay] 2026-04-22T23:20:17Z INFO ThreadId(01) audio packet received packet=3000",
|
||||
ConsoleLogLevel::Warn
|
||||
));
|
||||
assert!(!should_show_session_log_line(
|
||||
"[relay] 2026-04-22T23:20:17Z INFO ThreadId(04) decoded audio level rms=-32",
|
||||
ConsoleLogLevel::Warn
|
||||
));
|
||||
assert!(should_show_session_log_line(
|
||||
"[relay] 2026-04-22T23:20:17Z WARN pipeline is recovering",
|
||||
ConsoleLogLevel::Warn
|
||||
));
|
||||
assert!(should_show_session_log_line(
|
||||
"[relay] 2026-04-22T23:20:17Z INFO ❌ connect failed",
|
||||
ConsoleLogLevel::Error
|
||||
));
|
||||
assert!(should_show_session_log_line(
|
||||
"[launcher] Relay connected with inputs routed to remote.",
|
||||
ConsoleLogLevel::Error
|
||||
));
|
||||
assert!(should_show_session_log_line(
|
||||
"[relay] 2026-04-22T23:20:17Z INFO audio packet received",
|
||||
ConsoleLogLevel::Info
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_audio_gain_request_formats_live_control_file() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -73,7 +73,7 @@
|
||||
"client/src/launcher/diagnostics.rs": {
|
||||
"clippy_warnings": 92,
|
||||
"doc_debt": 12,
|
||||
"loc": 1021
|
||||
"loc": 1081
|
||||
},
|
||||
"client/src/launcher/mod.rs": {
|
||||
"clippy_warnings": 8,
|
||||
@ -96,19 +96,19 @@
|
||||
"loc": 1562
|
||||
},
|
||||
"client/src/launcher/ui.rs": {
|
||||
"clippy_warnings": 70,
|
||||
"clippy_warnings": 64,
|
||||
"doc_debt": 23,
|
||||
"loc": 2619
|
||||
"loc": 2635
|
||||
},
|
||||
"client/src/launcher/ui_components.rs": {
|
||||
"clippy_warnings": 22,
|
||||
"clippy_warnings": 12,
|
||||
"doc_debt": 18,
|
||||
"loc": 1515
|
||||
"loc": 1595
|
||||
},
|
||||
"client/src/launcher/ui_runtime.rs": {
|
||||
"clippy_warnings": 74,
|
||||
"doc_debt": 44,
|
||||
"loc": 1802
|
||||
"loc": 1923
|
||||
},
|
||||
"client/src/layout.rs": {
|
||||
"clippy_warnings": 6,
|
||||
|
||||
@ -49,8 +49,8 @@
|
||||
"loc": 564
|
||||
},
|
||||
"client/src/launcher/diagnostics.rs": {
|
||||
"line_percent": 84.3,
|
||||
"loc": 1021
|
||||
"line_percent": 84.98,
|
||||
"loc": 1081
|
||||
},
|
||||
"client/src/launcher/mod.rs": {
|
||||
"line_percent": 84.85,
|
||||
@ -62,7 +62,7 @@
|
||||
},
|
||||
"client/src/launcher/ui.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 2619
|
||||
"loc": 2635
|
||||
},
|
||||
"client/src/layout.rs": {
|
||||
"line_percent": 97.73,
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user