lesavka: show client and server versions

This commit is contained in:
Brad Stein 2026-04-16 15:59:42 -03:00
parent a3e84c5c15
commit 29944a5fd7
12 changed files with 78 additions and 12 deletions

View File

@ -97,6 +97,7 @@ mod tests {
let mut caps = PeerCaps {
camera: true,
microphone: false,
server_version: None,
camera_output: Some(String::from("uvc")),
camera_codec: Some(String::from("mjpeg")),
camera_width: Some(1280),

View File

@ -12,6 +12,7 @@ use tracing::{info, warn};
pub struct PeerCaps {
pub camera: bool,
pub microphone: bool,
pub server_version: Option<String>,
pub camera_output: Option<String>,
pub camera_codec: Option<String>,
pub camera_width: Option<u32>,
@ -64,6 +65,8 @@ pub async fn negotiate(uri: &str) -> PeerCaps {
PeerCaps {
camera: rsp.camera,
microphone: rsp.microphone,
server_version: (!rsp.server_version.is_empty())
.then_some(rsp.server_version.clone()),
camera_output: (!rsp.camera_output.is_empty()).then_some(rsp.camera_output.clone()),
camera_codec: (!rsp.camera_codec.is_empty()).then_some(rsp.camera_codec.clone()),
camera_width: (rsp.camera_width != 0).then_some(rsp.camera_width),
@ -125,6 +128,11 @@ pub async fn negotiate(uri: &str) -> PeerCaps {
let caps = PeerCaps {
camera: rsp.camera,
microphone: rsp.microphone,
server_version: if rsp.server_version.is_empty() {
None
} else {
Some(rsp.server_version.clone())
},
camera_output: if rsp.camera_output.is_empty() {
None
} else {

View File

@ -188,6 +188,7 @@ pub struct DeviceSelection {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LauncherState {
pub server_available: bool,
pub server_version: Option<String>,
pub routing: InputRouting,
pub view_mode: ViewMode,
pub displays: [DisplaySurface; 2],
@ -209,6 +210,7 @@ impl Default for LauncherState {
fn default() -> Self {
Self {
server_available: false,
server_version: None,
routing: InputRouting::Remote,
view_mode: ViewMode::Unified,
displays: [DisplaySurface::Preview, DisplaySurface::Preview],
@ -241,6 +243,17 @@ impl LauncherState {
self.server_available = available;
}
pub fn set_server_version(&mut self, version: Option<String>) {
self.server_version = version.and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
});
}
pub fn set_view_mode(&mut self, view_mode: ViewMode) {
self.view_mode = view_mode;
self.displays = match view_mode {

View File

@ -487,7 +487,11 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
server_entry.connect_changed(move |_| {
let server_addr =
selected_server_addr(&server_entry_read, server_addr_fallback.as_ref());
state.borrow_mut().set_server_available(false);
{
let mut state = state.borrow_mut();
state.set_server_available(false);
state.set_server_version(None);
}
if let Some(preview) = preview.as_ref() {
preview.set_server_addr(server_addr.clone());
}
@ -1404,6 +1408,10 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
while let Ok(message) = caps_rx.try_recv() {
match message {
CapsMessage::Refresh(caps) => {
{
let mut state = state.borrow_mut();
state.set_server_version(caps.server_version.clone());
}
if let (Some(width), Some(height)) =
(caps.eye_width, caps.eye_height)
{

View File

@ -125,21 +125,29 @@ pub fn build_launcher_view(
hero.set_hexpand(true);
let brand_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
let brand_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
brand_row.set_halign(gtk::Align::Start);
let heading = gtk::Label::new(Some("Lesavka"));
heading.add_css_class("title-2");
heading.set_halign(gtk::Align::Start);
brand_box.append(&heading);
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);
brand_row.append(&heading);
brand_row.append(&version_tag);
brand_box.append(&brand_row);
hero.append(&brand_box);
let chips = gtk::Box::new(gtk::Orientation::Horizontal, 6);
chips.set_halign(gtk::Align::End);
chips.set_hexpand(true);
let (relay_chip, relay_light, relay_value) = build_status_chip_with_light("Server", "Offline");
let (relay_chip, relay_light, relay_value) = build_status_chip_with_light("Server", "");
let (routing_chip, routing_light, routing_value) =
build_status_chip_with_light("Inputs", "Local");
let (gpio_chip, gpio_light, gpio_value) = build_status_chip_with_light("GPIO", "Unknown");
let (shortcut_chip, shortcut_value) = build_status_chip("Swap Key", "Pause");
stabilize_chip(&relay_chip, 84);
stabilize_chip(&relay_chip, 104);
stabilize_chip(&routing_chip, 84);
stabilize_chip(&gpio_chip, 84);
stabilize_chip(&shortcut_chip, 88);
@ -610,6 +618,11 @@ pub fn install_css(window: &gtk::ApplicationWindow) {
font-weight: 700;
opacity: 0.92;
}
label.version-tag {
font-size: 0.76rem;
opacity: 0.72;
margin-bottom: 3px;
}
box.status-chip {
background: rgba(91, 179, 162, 0.12);
border: 1px solid rgba(91, 179, 162, 0.25);

View File

@ -29,14 +29,22 @@ pub type RelayChild = Child;
pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, child_running: bool) {
let relay_live = child_running || state.remote_active;
set_status_light(&widgets.summary.relay_light, state.server_available);
widgets
.summary
.relay_value
.set_text(if state.server_available {
"Online"
} else {
"Offline"
});
widgets.summary.relay_value.set_text(
state
.server_version
.as_deref()
.map(|version| version.trim())
.filter(|version| !version.is_empty())
.map(|version| {
if version.starts_with('v') {
version.to_string()
} else {
format!("v{version}")
}
})
.as_deref()
.unwrap_or(""),
);
set_status_light(
&widgets.summary.routing_light,
matches!(state.routing, InputRouting::Remote),

View File

@ -2,6 +2,8 @@
#![forbid(unsafe_code)]
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub mod app;
mod app_support;
pub mod handshake;

View File

@ -53,6 +53,7 @@ message HandshakeSet {
uint32 eye_width = 8;
uint32 eye_height = 9;
uint32 eye_fps = 10;
string server_version = 11;
}
message Empty {}

View File

@ -33,6 +33,7 @@ impl Handshake for HandshakeSvc {
eye_width,
eye_height,
eye_fps,
server_version: crate::VERSION.to_string(),
}))
}
}

View File

@ -1,5 +1,7 @@
// server/src/lib.rs
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub mod audio;
pub mod camera;
pub mod camera_runtime;

View File

@ -7,16 +7,19 @@
//! devices.
mod handshake {
#[allow(dead_code)]
#[derive(Default, Clone, Debug)]
pub struct PeerCaps {
pub camera: bool,
pub microphone: bool,
pub server_version: Option<String>,
}
pub async fn negotiate(_uri: &str) -> PeerCaps {
PeerCaps {
camera: std::env::var("LESAVKA_TEST_CAP_CAMERA").is_ok(),
microphone: std::env::var("LESAVKA_TEST_CAP_MIC").is_ok(),
server_version: None,
}
}
}

View File

@ -54,6 +54,7 @@ async fn negotiate_against_local_server() -> PeerCaps {
fn assert_default_caps(caps: &PeerCaps) {
assert!(!caps.camera);
assert!(!caps.microphone);
assert_eq!(caps.server_version, None);
assert_eq!(caps.camera_output, None);
assert_eq!(caps.camera_codec, None);
assert_eq!(caps.camera_width, None);
@ -99,6 +100,7 @@ impl Handshake for SparseHandshakeSvc {
Ok(Response::new(HandshakeSet {
camera: true,
microphone: false,
server_version: String::new(),
camera_output: String::new(),
camera_codec: String::new(),
camera_width: 0,
@ -135,6 +137,10 @@ fn handshake_returns_uvc_caps_with_explicit_dimensions_and_fps() {
let caps = rt.block_on(negotiate_against_local_server());
assert!(caps.camera);
assert!(caps.microphone);
assert_eq!(
caps.server_version,
Some(lesavka_server::VERSION.to_string())
);
assert_eq!(caps.camera_output, Some(String::from("uvc")));
assert_eq!(caps.camera_codec, Some(String::from("mjpeg")));
assert_eq!(caps.camera_width, Some(1024));