launcher: stabilize routing and per-display breakout
This commit is contained in:
parent
7d4754ba31
commit
2f7cc44976
@ -6,6 +6,8 @@ use anyhow::{Context, Result};
|
|||||||
use evdev::{AbsoluteAxisCode, Device, EventType, KeyCode, RelativeAxisCode};
|
use evdev::{AbsoluteAxisCode, Device, EventType, KeyCode, RelativeAxisCode};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::broadcast::Sender,
|
sync::broadcast::Sender,
|
||||||
time::{Duration, interval},
|
time::{Duration, interval},
|
||||||
@ -35,6 +37,14 @@ pub struct InputAggregator {
|
|||||||
quick_toggle_down: bool,
|
quick_toggle_down: bool,
|
||||||
quick_toggle_debounce: Duration,
|
quick_toggle_debounce: Duration,
|
||||||
last_quick_toggle_at: Option<Instant>,
|
last_quick_toggle_at: Option<Instant>,
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
routing_control_path: Option<PathBuf>,
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
routing_control_marker: u128,
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
routing_state_path: Option<PathBuf>,
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
published_remote_capture: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputAggregator {
|
impl InputAggregator {
|
||||||
@ -55,6 +65,10 @@ impl InputAggregator {
|
|||||||
capture_remote_boot: bool,
|
capture_remote_boot: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let quick_toggle_key = quick_toggle_key_from_env();
|
let quick_toggle_key = quick_toggle_key_from_env();
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
let routing_control_path = launcher_routing_path_from_env("LESAVKA_LAUNCHER_INPUT_CONTROL");
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
let routing_state_path = launcher_routing_path_from_env("LESAVKA_LAUNCHER_INPUT_STATE");
|
||||||
Self {
|
Self {
|
||||||
kbd_tx,
|
kbd_tx,
|
||||||
mou_tx,
|
mou_tx,
|
||||||
@ -72,6 +86,17 @@ impl InputAggregator {
|
|||||||
quick_toggle_down: false,
|
quick_toggle_down: false,
|
||||||
quick_toggle_debounce: quick_toggle_debounce_from_env(),
|
quick_toggle_debounce: quick_toggle_debounce_from_env(),
|
||||||
last_quick_toggle_at: None,
|
last_quick_toggle_at: None,
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
routing_control_marker: routing_control_path
|
||||||
|
.as_deref()
|
||||||
|
.map(path_marker)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
routing_control_path,
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
routing_state_path,
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
published_remote_capture: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,12 +292,14 @@ impl InputAggregator {
|
|||||||
// Example approach: poll each aggregator in a simple loop
|
// Example approach: poll each aggregator in a simple loop
|
||||||
let mut tick = interval(Duration::from_millis(10));
|
let mut tick = interval(Duration::from_millis(10));
|
||||||
let mut current = Layout::SideBySide;
|
let mut current = Layout::SideBySide;
|
||||||
|
self.publish_routing_state_if_changed();
|
||||||
loop {
|
loop {
|
||||||
let mut want_kill = false;
|
let mut want_kill = false;
|
||||||
for kbd in &mut self.keyboards {
|
for kbd in &mut self.keyboards {
|
||||||
kbd.process_events();
|
kbd.process_events();
|
||||||
want_kill |= kbd.magic_kill();
|
want_kill |= kbd.magic_kill();
|
||||||
}
|
}
|
||||||
|
self.poll_launcher_routing_request();
|
||||||
let quick_toggle_now = self.quick_toggle_active();
|
let quick_toggle_now = self.quick_toggle_active();
|
||||||
self.observe_quick_toggle(quick_toggle_now);
|
self.observe_quick_toggle(quick_toggle_now);
|
||||||
let magic_now = self.keyboards.iter().any(|k| k.magic_grab());
|
let magic_now = self.keyboards.iter().any(|k| k.magic_grab());
|
||||||
@ -328,6 +355,7 @@ impl InputAggregator {
|
|||||||
if !self.pending_kill {
|
if !self.pending_kill {
|
||||||
focus_launcher_on_local_if_enabled();
|
focus_launcher_on_local_if_enabled();
|
||||||
}
|
}
|
||||||
|
self.publish_routing_state_if_changed();
|
||||||
if self.pending_kill {
|
if self.pending_kill {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -355,32 +383,42 @@ impl InputAggregator {
|
|||||||
tracing::info!("🧙 magic chord - freeing devices 🪄 EXPELLIARMUS!!! 🔓🕊️");
|
tracing::info!("🧙 magic chord - freeing devices 🪄 EXPELLIARMUS!!! 🔓🕊️");
|
||||||
}
|
}
|
||||||
if self.released {
|
if self.released {
|
||||||
for k in &mut self.keyboards {
|
self.enable_remote_capture();
|
||||||
k.reset_state();
|
#[cfg(not(coverage))]
|
||||||
k.set_send(true);
|
self.publish_routing_state_if_changed();
|
||||||
k.set_grab(true);
|
|
||||||
}
|
|
||||||
for m in &mut self.mice {
|
|
||||||
m.reset_state();
|
|
||||||
m.set_send(true);
|
|
||||||
m.set_grab(true);
|
|
||||||
}
|
|
||||||
self.released = false;
|
|
||||||
self.pending_release = false;
|
|
||||||
} else {
|
} else {
|
||||||
for k in &mut self.keyboards {
|
self.begin_local_release();
|
||||||
k.send_empty_report();
|
|
||||||
k.set_send(false);
|
|
||||||
}
|
|
||||||
for m in &mut self.mice {
|
|
||||||
m.reset_state();
|
|
||||||
m.set_send(false);
|
|
||||||
}
|
|
||||||
self.pending_release = true;
|
|
||||||
self.capture_pending_keys();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enable_remote_capture(&mut self) {
|
||||||
|
for k in &mut self.keyboards {
|
||||||
|
k.reset_state();
|
||||||
|
k.set_send(true);
|
||||||
|
k.set_grab(true);
|
||||||
|
}
|
||||||
|
for m in &mut self.mice {
|
||||||
|
m.reset_state();
|
||||||
|
m.set_send(true);
|
||||||
|
m.set_grab(true);
|
||||||
|
}
|
||||||
|
self.released = false;
|
||||||
|
self.pending_release = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_local_release(&mut self) {
|
||||||
|
for k in &mut self.keyboards {
|
||||||
|
k.send_empty_report();
|
||||||
|
k.set_send(false);
|
||||||
|
}
|
||||||
|
for m in &mut self.mice {
|
||||||
|
m.reset_state();
|
||||||
|
m.set_send(false);
|
||||||
|
}
|
||||||
|
self.pending_release = true;
|
||||||
|
self.capture_pending_keys();
|
||||||
|
}
|
||||||
|
|
||||||
fn capture_pending_keys(&mut self) {
|
fn capture_pending_keys(&mut self) {
|
||||||
self.pending_keys.clear();
|
self.pending_keys.clear();
|
||||||
for k in &self.keyboards {
|
for k in &self.keyboards {
|
||||||
@ -414,6 +452,47 @@ impl InputAggregator {
|
|||||||
}
|
}
|
||||||
self.quick_toggle_down = quick_toggle_now;
|
self.quick_toggle_down = quick_toggle_now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn poll_launcher_routing_request(&mut self) {
|
||||||
|
let Some(path) = self.routing_control_path.as_deref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let marker = path_marker(path);
|
||||||
|
if marker <= self.routing_control_marker {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.routing_control_marker = marker;
|
||||||
|
let Some(remote_capture) = read_launcher_routing_request(path) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if self.pending_release || self.pending_kill || remote_capture == !self.released {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if remote_capture {
|
||||||
|
info!("🎛️ launcher requested remote input capture");
|
||||||
|
self.enable_remote_capture();
|
||||||
|
self.publish_routing_state_if_changed();
|
||||||
|
} else {
|
||||||
|
info!("🎛️ launcher requested local input capture");
|
||||||
|
self.begin_local_release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn publish_routing_state_if_changed(&mut self) {
|
||||||
|
let remote_capture = !self.released;
|
||||||
|
if self.published_remote_capture == Some(remote_capture) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(path) = self.routing_state_path.as_deref() {
|
||||||
|
let _ = std::fs::write(
|
||||||
|
path,
|
||||||
|
if remote_capture { "remote\n" } else { "local\n" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.published_remote_capture = Some(remote_capture);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The classification function
|
/// The classification function
|
||||||
@ -560,3 +639,31 @@ fn focus_launcher_on_local_if_enabled() {
|
|||||||
.args(["-a", &title])
|
.args(["-a", &title])
|
||||||
.status();
|
.status();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn launcher_routing_path_from_env(key: &str) -> Option<PathBuf> {
|
||||||
|
std::env::var(key)
|
||||||
|
.ok()
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.filter(|path| !path.as_os_str().is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn read_launcher_routing_request(path: &Path) -> Option<bool> {
|
||||||
|
let raw = std::fs::read_to_string(path).ok()?;
|
||||||
|
match raw.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"remote" => Some(true),
|
||||||
|
"local" => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn path_marker(path: &Path) -> u128 {
|
||||||
|
std::fs::metadata(path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|meta| meta.modified().ok())
|
||||||
|
.and_then(|stamp| stamp.duration_since(std::time::UNIX_EPOCH).ok())
|
||||||
|
.map(|duration| duration.as_millis())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|||||||
@ -31,6 +31,13 @@ pub struct LauncherPreview {
|
|||||||
feeds: [PreviewFeed; 2],
|
feeds: [PreviewFeed; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PreviewBinding {
|
||||||
|
enabled: Arc<AtomicBool>,
|
||||||
|
alive: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
impl LauncherPreview {
|
impl LauncherPreview {
|
||||||
pub fn new(server_addr: String) -> Result<Self> {
|
pub fn new(server_addr: String) -> Result<Self> {
|
||||||
@ -48,48 +55,59 @@ impl LauncherPreview {
|
|||||||
monitor_id: usize,
|
monitor_id: usize,
|
||||||
picture: >k::Picture,
|
picture: >k::Picture,
|
||||||
status_label: >k::Label,
|
status_label: >k::Label,
|
||||||
) {
|
) -> Option<PreviewBinding> {
|
||||||
if let Some(feed) = self.feeds.get(monitor_id) {
|
self.feeds
|
||||||
feed.install_on_picture(picture, status_label);
|
.get(monitor_id)
|
||||||
}
|
.map(|feed| feed.install_on_picture(picture, status_label))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
impl PreviewBinding {
|
||||||
|
pub fn set_enabled(&self, enabled: bool) {
|
||||||
|
self.enabled.store(enabled, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_enabled(&self, enabled: bool) {
|
pub fn close(&self) {
|
||||||
for feed in &self.feeds {
|
self.alive.store(false, Ordering::Relaxed);
|
||||||
feed.set_enabled(enabled);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
struct PreviewFeed {
|
struct PreviewFeed {
|
||||||
latest: Arc<Mutex<Option<PreviewFrame>>>,
|
latest: Arc<Mutex<Option<PreviewFrame>>>,
|
||||||
enabled: Arc<AtomicBool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
impl PreviewFeed {
|
impl PreviewFeed {
|
||||||
fn spawn(server_addr: String, monitor_id: u32) -> Result<Self> {
|
fn spawn(server_addr: String, monitor_id: u32) -> Result<Self> {
|
||||||
let latest = Arc::new(Mutex::new(None));
|
let latest = Arc::new(Mutex::new(None));
|
||||||
let enabled = Arc::new(AtomicBool::new(true));
|
|
||||||
let store = Arc::clone(&latest);
|
let store = Arc::clone(&latest);
|
||||||
let enabled_flag = Arc::clone(&enabled);
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
if let Err(err) = run_preview_feed(server_addr, monitor_id, store, enabled_flag) {
|
if let Err(err) = run_preview_feed(server_addr, monitor_id, store) {
|
||||||
warn!(monitor_id, ?err, "launcher preview feed exited");
|
warn!(monitor_id, ?err, "launcher preview feed exited");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Ok(Self { latest, enabled })
|
Ok(Self { latest })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install_on_picture(&self, picture: >k::Picture, status_label: >k::Label) {
|
fn install_on_picture(
|
||||||
|
&self,
|
||||||
|
picture: >k::Picture,
|
||||||
|
status_label: >k::Label,
|
||||||
|
) -> PreviewBinding {
|
||||||
let picture = picture.clone();
|
let picture = picture.clone();
|
||||||
let status_label = status_label.clone();
|
let status_label = status_label.clone();
|
||||||
let latest = Arc::clone(&self.latest);
|
let latest = Arc::clone(&self.latest);
|
||||||
let enabled = Arc::clone(&self.enabled);
|
let enabled = Arc::new(AtomicBool::new(true));
|
||||||
|
let alive = Arc::new(AtomicBool::new(true));
|
||||||
|
let enabled_flag = Arc::clone(&enabled);
|
||||||
|
let alive_flag = Arc::clone(&alive);
|
||||||
glib::timeout_add_local(Duration::from_millis(120), move || {
|
glib::timeout_add_local(Duration::from_millis(120), move || {
|
||||||
if !enabled.load(Ordering::Relaxed) {
|
if !alive_flag.load(Ordering::Relaxed) {
|
||||||
status_label.set_text("Paused for pop-out windows");
|
return glib::ControlFlow::Break;
|
||||||
|
}
|
||||||
|
if !enabled_flag.load(Ordering::Relaxed) {
|
||||||
return glib::ControlFlow::Continue;
|
return glib::ControlFlow::Continue;
|
||||||
}
|
}
|
||||||
let next = latest.lock().ok().and_then(|mut slot| slot.take());
|
let next = latest.lock().ok().and_then(|mut slot| slot.take());
|
||||||
@ -107,15 +125,7 @@ impl PreviewFeed {
|
|||||||
}
|
}
|
||||||
glib::ControlFlow::Continue
|
glib::ControlFlow::Continue
|
||||||
});
|
});
|
||||||
}
|
PreviewBinding { enabled, alive }
|
||||||
|
|
||||||
fn set_enabled(&self, enabled: bool) {
|
|
||||||
self.enabled.store(enabled, Ordering::Relaxed);
|
|
||||||
if !enabled {
|
|
||||||
if let Ok(mut slot) = self.latest.lock() {
|
|
||||||
*slot = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +142,6 @@ fn run_preview_feed(
|
|||||||
server_addr: String,
|
server_addr: String,
|
||||||
monitor_id: u32,
|
monitor_id: u32,
|
||||||
latest: Arc<Mutex<Option<PreviewFrame>>>,
|
latest: Arc<Mutex<Option<PreviewFrame>>>,
|
||||||
enabled: Arc<AtomicBool>,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (pipeline, appsrc, appsink) = build_preview_pipeline()?;
|
let (pipeline, appsrc, appsink) = build_preview_pipeline()?;
|
||||||
pipeline
|
pipeline
|
||||||
@ -162,10 +171,6 @@ fn run_preview_feed(
|
|||||||
|
|
||||||
let _ = rt.block_on(async move {
|
let _ = rt.block_on(async move {
|
||||||
loop {
|
loop {
|
||||||
if !enabled.load(Ordering::Relaxed) {
|
|
||||||
tokio::time::sleep(Duration::from_millis(120)).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let channel = match Channel::from_shared(server_addr.clone()) {
|
let channel = match Channel::from_shared(server_addr.clone()) {
|
||||||
Ok(endpoint) => match endpoint.tcp_nodelay(true).connect().await {
|
Ok(endpoint) => match endpoint.tcp_nodelay(true).connect().await {
|
||||||
Ok(channel) => channel,
|
Ok(channel) => channel,
|
||||||
@ -190,9 +195,6 @@ fn run_preview_feed(
|
|||||||
Ok(mut stream) => {
|
Ok(mut stream) => {
|
||||||
debug!(monitor_id, "launcher preview connected");
|
debug!(monitor_id, "launcher preview connected");
|
||||||
while let Some(item) = stream.get_mut().message().await.transpose() {
|
while let Some(item) = stream.get_mut().message().await.transpose() {
|
||||||
if !enabled.load(Ordering::Relaxed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
match item {
|
match item {
|
||||||
Ok(pkt) => push_preview_packet(&appsrc, pkt),
|
Ok(pkt) => push_preview_packet(&appsrc, pkt),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@ -32,6 +32,21 @@ impl ViewMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum DisplaySurface {
|
||||||
|
Preview,
|
||||||
|
Window,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplaySurface {
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Preview => "preview",
|
||||||
|
Self::Window => "window",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
pub struct DeviceSelection {
|
pub struct DeviceSelection {
|
||||||
pub camera: Option<String>,
|
pub camera: Option<String>,
|
||||||
@ -43,6 +58,7 @@ pub struct DeviceSelection {
|
|||||||
pub struct LauncherState {
|
pub struct LauncherState {
|
||||||
pub routing: InputRouting,
|
pub routing: InputRouting,
|
||||||
pub view_mode: ViewMode,
|
pub view_mode: ViewMode,
|
||||||
|
pub displays: [DisplaySurface; 2],
|
||||||
pub devices: DeviceSelection,
|
pub devices: DeviceSelection,
|
||||||
pub remote_active: bool,
|
pub remote_active: bool,
|
||||||
pub notes: Vec<String>,
|
pub notes: Vec<String>,
|
||||||
@ -53,6 +69,7 @@ impl Default for LauncherState {
|
|||||||
Self {
|
Self {
|
||||||
routing: InputRouting::Remote,
|
routing: InputRouting::Remote,
|
||||||
view_mode: ViewMode::Unified,
|
view_mode: ViewMode::Unified,
|
||||||
|
displays: [DisplaySurface::Preview, DisplaySurface::Preview],
|
||||||
devices: DeviceSelection::default(),
|
devices: DeviceSelection::default(),
|
||||||
remote_active: false,
|
remote_active: false,
|
||||||
notes: Vec::new(),
|
notes: Vec::new(),
|
||||||
@ -71,6 +88,39 @@ impl LauncherState {
|
|||||||
|
|
||||||
pub fn set_view_mode(&mut self, view_mode: ViewMode) {
|
pub fn set_view_mode(&mut self, view_mode: ViewMode) {
|
||||||
self.view_mode = view_mode;
|
self.view_mode = view_mode;
|
||||||
|
self.displays = match view_mode {
|
||||||
|
ViewMode::Unified => [DisplaySurface::Preview, DisplaySurface::Preview],
|
||||||
|
ViewMode::Breakout => [DisplaySurface::Window, DisplaySurface::Window],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_surface(&self, monitor_id: usize) -> DisplaySurface {
|
||||||
|
self.displays
|
||||||
|
.get(monitor_id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(DisplaySurface::Preview)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_display_surface(&mut self, monitor_id: usize, surface: DisplaySurface) {
|
||||||
|
if let Some(slot) = self.displays.get_mut(monitor_id) {
|
||||||
|
*slot = surface;
|
||||||
|
self.view_mode = if self
|
||||||
|
.displays
|
||||||
|
.iter()
|
||||||
|
.any(|display| matches!(display, DisplaySurface::Window))
|
||||||
|
{
|
||||||
|
ViewMode::Breakout
|
||||||
|
} else {
|
||||||
|
ViewMode::Unified
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn breakout_count(&self) -> usize {
|
||||||
|
self.displays
|
||||||
|
.iter()
|
||||||
|
.filter(|surface| matches!(surface, DisplaySurface::Window))
|
||||||
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_camera(&mut self, camera: Option<String>) {
|
pub fn select_camera(&mut self, camera: Option<String>) {
|
||||||
@ -119,7 +169,7 @@ impl LauncherState {
|
|||||||
|
|
||||||
pub fn status_line(&self) -> String {
|
pub fn status_line(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
"mode={} view={} active={} camera={} mic={} speaker={}",
|
"mode={} view={} active={} d1={} d2={} camera={} mic={} speaker={}",
|
||||||
match self.routing {
|
match self.routing {
|
||||||
InputRouting::Local => "local",
|
InputRouting::Local => "local",
|
||||||
InputRouting::Remote => "remote",
|
InputRouting::Remote => "remote",
|
||||||
@ -129,6 +179,8 @@ impl LauncherState {
|
|||||||
ViewMode::Breakout => "breakout",
|
ViewMode::Breakout => "breakout",
|
||||||
},
|
},
|
||||||
self.remote_active,
|
self.remote_active,
|
||||||
|
self.displays[0].label(),
|
||||||
|
self.displays[1].label(),
|
||||||
self.devices.camera.as_deref().unwrap_or("auto"),
|
self.devices.camera.as_deref().unwrap_or("auto"),
|
||||||
self.devices.microphone.as_deref().unwrap_or("auto"),
|
self.devices.microphone.as_deref().unwrap_or("auto"),
|
||||||
self.devices.speaker.as_deref().unwrap_or("auto"),
|
self.devices.speaker.as_deref().unwrap_or("auto"),
|
||||||
@ -164,12 +216,30 @@ mod tests {
|
|||||||
let state = LauncherState::new();
|
let state = LauncherState::new();
|
||||||
assert_eq!(state.routing, InputRouting::Remote);
|
assert_eq!(state.routing, InputRouting::Remote);
|
||||||
assert_eq!(state.view_mode, ViewMode::Unified);
|
assert_eq!(state.view_mode, ViewMode::Unified);
|
||||||
|
assert_eq!(state.display_surface(0), DisplaySurface::Preview);
|
||||||
|
assert_eq!(state.display_surface(1), DisplaySurface::Preview);
|
||||||
assert!(!state.remote_active);
|
assert!(!state.remote_active);
|
||||||
assert!(state.devices.camera.is_none());
|
assert!(state.devices.camera.is_none());
|
||||||
assert!(state.devices.microphone.is_none());
|
assert!(state.devices.microphone.is_none());
|
||||||
assert!(state.devices.speaker.is_none());
|
assert!(state.devices.speaker.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_surface_updates_global_view_summary() {
|
||||||
|
let mut state = LauncherState::new();
|
||||||
|
state.set_display_surface(1, DisplaySurface::Window);
|
||||||
|
assert_eq!(state.view_mode, ViewMode::Breakout);
|
||||||
|
assert_eq!(state.breakout_count(), 1);
|
||||||
|
|
||||||
|
state.set_display_surface(1, DisplaySurface::Preview);
|
||||||
|
assert_eq!(state.view_mode, ViewMode::Unified);
|
||||||
|
assert_eq!(state.breakout_count(), 0);
|
||||||
|
|
||||||
|
state.set_view_mode(ViewMode::Breakout);
|
||||||
|
assert_eq!(state.display_surface(0), DisplaySurface::Window);
|
||||||
|
assert_eq!(state.display_surface(1), DisplaySurface::Window);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn selecting_auto_or_blank_clears_explicit_device() {
|
fn selecting_auto_or_blank_clears_explicit_device() {
|
||||||
let mut state = LauncherState::new();
|
let mut state = LauncherState::new();
|
||||||
@ -227,6 +297,8 @@ mod tests {
|
|||||||
assert!(status.contains("mode=local"));
|
assert!(status.contains("mode=local"));
|
||||||
assert!(status.contains("view=unified"));
|
assert!(status.contains("view=unified"));
|
||||||
assert!(status.contains("active=true"));
|
assert!(status.contains("active=true"));
|
||||||
|
assert!(status.contains("d1=preview"));
|
||||||
|
assert!(status.contains("d2=preview"));
|
||||||
assert!(status.contains("camera=/dev/video0"));
|
assert!(status.contains("camera=/dev/video0"));
|
||||||
assert!(status.contains("mic=alsa_input.usb"));
|
assert!(status.contains("mic=alsa_input.usb"));
|
||||||
assert!(status.contains("speaker=alsa_output.usb"));
|
assert!(status.contains("speaker=alsa_output.usb"));
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -22,8 +22,8 @@
|
|||||||
},
|
},
|
||||||
"client/src/input/inputs.rs": {
|
"client/src/input/inputs.rs": {
|
||||||
"clippy_warnings": 42,
|
"clippy_warnings": 42,
|
||||||
"doc_debt": 11,
|
"doc_debt": 16,
|
||||||
"loc": 562
|
"loc": 669
|
||||||
},
|
},
|
||||||
"client/src/input/keyboard.rs": {
|
"client/src/input/keyboard.rs": {
|
||||||
"clippy_warnings": 24,
|
"clippy_warnings": 24,
|
||||||
@ -71,19 +71,19 @@
|
|||||||
"loc": 182
|
"loc": 182
|
||||||
},
|
},
|
||||||
"client/src/launcher/preview.rs": {
|
"client/src/launcher/preview.rs": {
|
||||||
"clippy_warnings": 22,
|
"clippy_warnings": 20,
|
||||||
"doc_debt": 9,
|
"doc_debt": 6,
|
||||||
"loc": 291
|
"loc": 293
|
||||||
},
|
},
|
||||||
"client/src/launcher/state.rs": {
|
"client/src/launcher/state.rs": {
|
||||||
"clippy_warnings": 8,
|
"clippy_warnings": 14,
|
||||||
"doc_debt": 9,
|
"doc_debt": 13,
|
||||||
"loc": 234
|
"loc": 306
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui.rs": {
|
"client/src/launcher/ui.rs": {
|
||||||
"clippy_warnings": 8,
|
"clippy_warnings": 18,
|
||||||
"doc_debt": 8,
|
"doc_debt": 15,
|
||||||
"loc": 838
|
"loc": 996
|
||||||
},
|
},
|
||||||
"client/src/layout.rs": {
|
"client/src/layout.rs": {
|
||||||
"clippy_warnings": 6,
|
"clippy_warnings": 6,
|
||||||
|
|||||||
@ -17,8 +17,8 @@
|
|||||||
"loc": 368
|
"loc": 368
|
||||||
},
|
},
|
||||||
"client/src/input/inputs.rs": {
|
"client/src/input/inputs.rs": {
|
||||||
"line_percent": 97.32,
|
"line_percent": 97.55,
|
||||||
"loc": 562
|
"loc": 669
|
||||||
},
|
},
|
||||||
"client/src/input/keyboard.rs": {
|
"client/src/input/keyboard.rs": {
|
||||||
"line_percent": 95.7,
|
"line_percent": 95.7,
|
||||||
@ -53,12 +53,12 @@
|
|||||||
"loc": 181
|
"loc": 181
|
||||||
},
|
},
|
||||||
"client/src/launcher/state.rs": {
|
"client/src/launcher/state.rs": {
|
||||||
"line_percent": 97.97297297297297,
|
"line_percent": 98.0,
|
||||||
"loc": 234
|
"loc": 306
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui.rs": {
|
"client/src/launcher/ui.rs": {
|
||||||
"line_percent": 100.0,
|
"line_percent": 100.0,
|
||||||
"loc": 838
|
"loc": 996
|
||||||
},
|
},
|
||||||
"client/src/layout.rs": {
|
"client/src/layout.rs": {
|
||||||
"line_percent": 97.72727272727273,
|
"line_percent": 97.72727272727273,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user