Support multiple monitors
For heads-up displays with multiple monitors, we're going to want one Firefox window on each. To support this, we need to get a list of connected monitors from the operating system and associate each with its own window. Since Firefox may start with multiple taps open automatically, we first close all but one and associate it with the first monitor. Then, for each remaining monitor, we open a new window to associate with it. To maintain the monitor-window association, the `Session` structure has a `HashMap`. When a naigation request arrives, the Firefox window to control is found by looking up the specified screen name in the map. Since the Marionette protocol is stateful, we have to "switch to" the desired window and then send the navigation command. I have tried to design the monitor information lookup API so that it can be swapped out at compile time for different operating systems. For now, only X11 is supported, but we could hypothetically support Wayland or even Windows by implementing the appropriate `get_monitors` function for those APIs.dev/ci
parent
4eba92f4a0
commit
09342acb01
|
@ -386,6 +386,7 @@ dependencies = [
|
|||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"x11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -935,6 +936,16 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11"
|
||||
version = "2.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2638d5b9c17ac40575fb54bb461a4b1d2a8d1b4ffcc4ff237d254ec59ddeb82"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.4"
|
||||
|
|
|
@ -16,3 +16,4 @@ tokio-stream = "0.1.11"
|
|||
toml = "0.5.10"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter", "fmt"] }
|
||||
x11 = { version = "2.20.1", features = ["xlib", "xrandr"] }
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
mod browser;
|
||||
mod config;
|
||||
mod marionette;
|
||||
mod monitor;
|
||||
mod mqtt;
|
||||
mod session;
|
||||
#[cfg(unix)]
|
||||
mod x11;
|
||||
|
||||
use tokio::signal::unix::{self, SignalKind};
|
||||
use tracing::info;
|
||||
|
|
|
@ -83,6 +83,21 @@ pub struct NavigateParams {
|
|||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(dead_code)]
|
||||
pub struct CloseWindowParams {
|
||||
pub handle: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(dead_code)]
|
||||
pub struct SwitchToWindowParams {
|
||||
pub handle: String,
|
||||
pub focus: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "command", content = "params")]
|
||||
pub enum Command {
|
||||
|
@ -94,4 +109,12 @@ pub enum Command {
|
|||
Navigate(NavigateParams),
|
||||
#[serde(rename = "WebDriver:GetCurrentURL")]
|
||||
GetCurrentUrl,
|
||||
#[serde(rename = "WebDriver:GetWindowHandles")]
|
||||
GetWindowHandles,
|
||||
#[serde(rename = "WebDriver:CloseWindow")]
|
||||
CloseWindow(CloseWindowParams),
|
||||
#[serde(rename = "WebDriver:NewWindow")]
|
||||
NewWindow,
|
||||
#[serde(rename = "WebDriver:SwitchToWindow")]
|
||||
SwitchToWindow(SwitchToWindowParams),
|
||||
}
|
||||
|
|
|
@ -18,8 +18,9 @@ use tracing::{debug, error, trace, warn};
|
|||
|
||||
pub use error::{CommandError, ConnectionError, ErrorResponse, MessageError};
|
||||
use message::{
|
||||
Command, GetCurrentUrlResponse, GetTitleResponse, Hello, NavigateParams,
|
||||
NewSessionParams, NewSessionResponse,
|
||||
CloseWindowParams, Command, GetCurrentUrlResponse, GetTitleResponse,
|
||||
Hello, NavigateParams, NewSessionParams, NewSessionResponse,
|
||||
SwitchToWindowParams,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
@ -167,6 +168,21 @@ impl Marionette {
|
|||
Self { conn }
|
||||
}
|
||||
|
||||
pub async fn close_window(
|
||||
&mut self,
|
||||
handle: impl Into<String>,
|
||||
) -> Result<(), CommandError> {
|
||||
let res: Vec<String> = self
|
||||
.conn
|
||||
.send_message(Command::CloseWindow(CloseWindowParams {
|
||||
handle: handle.into(),
|
||||
}))
|
||||
.await?
|
||||
.unwrap();
|
||||
debug!("Received message: {:?}", res);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_title(&mut self) -> Result<String, CommandError> {
|
||||
let res: GetTitleResponse =
|
||||
self.conn.send_message(Command::GetTitle).await?.unwrap();
|
||||
|
@ -175,12 +191,27 @@ impl Marionette {
|
|||
}
|
||||
|
||||
pub async fn get_current_url(&mut self) -> Result<String, CommandError> {
|
||||
let res: GetCurrentUrlResponse =
|
||||
self.conn.send_message(Command::GetCurrentUrl).await?.unwrap();
|
||||
let res: GetCurrentUrlResponse = self
|
||||
.conn
|
||||
.send_message(Command::GetCurrentUrl)
|
||||
.await?
|
||||
.unwrap();
|
||||
debug!("Received message: {:?}", res);
|
||||
Ok(res.value)
|
||||
}
|
||||
|
||||
pub async fn get_window_handles(
|
||||
&mut self,
|
||||
) -> Result<Vec<String>, CommandError> {
|
||||
let res = self
|
||||
.conn
|
||||
.send_message(Command::GetWindowHandles)
|
||||
.await?
|
||||
.unwrap();
|
||||
debug!("Received message: {:?}", res);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn navigate<U>(&mut self, url: U) -> Result<(), CommandError>
|
||||
where
|
||||
U: Into<String>,
|
||||
|
@ -208,4 +239,28 @@ impl Marionette {
|
|||
debug!("Received message: {:?}", res);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn new_window(&mut self) -> Result<String, CommandError> {
|
||||
let res: String =
|
||||
self.conn.send_message(Command::NewWindow).await?.unwrap();
|
||||
debug!("Received message: {:?}", res);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn switch_to_window(
|
||||
&mut self,
|
||||
handle: String,
|
||||
focus: bool,
|
||||
) -> Result<(), CommandError> {
|
||||
let res: serde_json::Value = self
|
||||
.conn
|
||||
.send_message(Command::SwitchToWindow(SwitchToWindowParams {
|
||||
handle,
|
||||
focus,
|
||||
}))
|
||||
.await?
|
||||
.unwrap();
|
||||
debug!("Received message: {:?}", res);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
#[derive(Debug)]
|
||||
pub struct Monitor {
|
||||
pub name: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use crate::browser::{Browser, BrowserError};
|
||||
use crate::config::Configuration;
|
||||
use crate::marionette::error::{CommandError, ConnectionError};
|
||||
use crate::marionette::{Marionette, MarionetteConnection};
|
||||
use crate::monitor::Monitor;
|
||||
use crate::mqtt::{Message, MqttClient, MqttPublisher};
|
||||
#[cfg(unix)]
|
||||
use crate::x11::{xrandr, Display};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SessionError {
|
||||
|
@ -94,6 +98,14 @@ impl Session {
|
|||
}
|
||||
|
||||
pub async fn run(mut self) {
|
||||
let windows = match self.init_windows().await {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
error!("Failed to initialize browser windows: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut client = MqttClient::new(&self.config).unwrap();
|
||||
loop {
|
||||
if let Err(e) = client.connect().await {
|
||||
|
@ -108,15 +120,39 @@ impl Session {
|
|||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let handler = MessageHandler {
|
||||
marionette: &mut self.marionette,
|
||||
windows,
|
||||
};
|
||||
client.run(handler).await;
|
||||
}
|
||||
|
||||
async fn init_windows(
|
||||
&mut self,
|
||||
) -> Result<HashMap<String, String>, SessionError> {
|
||||
debug!("Getting monitor configuration");
|
||||
let monitors = get_monitors();
|
||||
let mut handles = self.marionette.get_window_handles().await?;
|
||||
for handle in handles.iter().take(handles.len() - 1) {
|
||||
self.marionette.close_window(handle).await?;
|
||||
}
|
||||
let mut windowmap = HashMap::new();
|
||||
let mut window = Some(handles.remove(handles.len() - 1));
|
||||
for monitor in monitors {
|
||||
if window.is_none() {
|
||||
window = Some(self.marionette.new_window().await?);
|
||||
}
|
||||
windowmap.insert(monitor.name, window.take().unwrap());
|
||||
}
|
||||
trace!("Built window map: {:?}", windowmap);
|
||||
Ok(windowmap)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MessageHandler<'a> {
|
||||
marionette: &'a mut Marionette,
|
||||
windows: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
|
@ -131,6 +167,21 @@ impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> {
|
|||
}
|
||||
Some(s) => s,
|
||||
};
|
||||
if let Some(window) = self.windows.get(*screen) {
|
||||
debug!("Switching to window {}", window);
|
||||
if let Err(e) =
|
||||
self.marionette.switch_to_window(window.into(), false).await
|
||||
{
|
||||
error!(
|
||||
"Failed to switch to window on screen {}: {}",
|
||||
screen, e
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
error!("Invalid navigate request: unknown screen {}", screen);
|
||||
return;
|
||||
}
|
||||
debug!("Handling navigate request: {}", url);
|
||||
info!("Navigate screen {} to {}", screen, url);
|
||||
if let Err(e) = self.marionette.navigate(url.to_string()).await {
|
||||
|
@ -154,3 +205,16 @@ impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn get_monitors() -> Vec<Monitor> {
|
||||
let display = Display::open().unwrap();
|
||||
xrandr::get_monitors(&display)
|
||||
.iter()
|
||||
.map(|m| Monitor {
|
||||
name: m.name().unwrap().to_string(),
|
||||
width: m.width(),
|
||||
height: m.height(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
pub mod xrandr;
|
||||
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::{c_int, c_ulong};
|
||||
|
||||
use x11::xlib::{
|
||||
XCloseDisplay, XDefaultScreen, XDisplayName, XOpenDisplay, XRootWindow,
|
||||
_XDisplay,
|
||||
};
|
||||
|
||||
/// Error returned if connecting to the X server fails
|
||||
#[derive(Debug)]
|
||||
pub struct OpenDisplayError;
|
||||
|
||||
/// Wrapper for an X display pointer
|
||||
#[derive(Debug)]
|
||||
pub struct Display {
|
||||
display: *mut _XDisplay,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
/// Open a connection to the X server
|
||||
///
|
||||
/// If the connection succeeds, a [`Display`] is returned. Otherwise, an
|
||||
/// [`OpenDisplayError`] is returned.
|
||||
pub fn open() -> Result<Self, OpenDisplayError> {
|
||||
let display = unsafe { XOpenDisplay(std::ptr::null()) };
|
||||
if display.is_null() {
|
||||
Err(OpenDisplayError)
|
||||
} else {
|
||||
Ok(Self { display })
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the name of the X server display
|
||||
///
|
||||
/// If the display name cannot be determined, an empty string is returned.
|
||||
pub fn name() -> String {
|
||||
let name = unsafe { CStr::from_ptr(XDisplayName(std::ptr::null())) };
|
||||
let name = name.to_str().unwrap_or("");
|
||||
String::from(name)
|
||||
}
|
||||
|
||||
/// Return the default screen number of the display
|
||||
pub fn default_screen(&self) -> c_int {
|
||||
unsafe { XDefaultScreen(self.display) }
|
||||
}
|
||||
|
||||
/// Return the ID of the root window
|
||||
pub fn root_window(&self) -> c_ulong {
|
||||
let screen = self.default_screen();
|
||||
unsafe { XRootWindow(self.display, screen) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Display {
|
||||
fn drop(&mut self) {
|
||||
unsafe { XCloseDisplay(self.display) };
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_display_open() {
|
||||
let _dpy = Display::open().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name() {
|
||||
assert!(!Display::name().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_screen() {
|
||||
let dpy = Display::open().unwrap();
|
||||
assert!(dpy.default_screen() > -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_root_window() {
|
||||
let dpy = Display::open().unwrap();
|
||||
assert!(dpy.root_window() > 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/// High-level interface for the X11 RandR extension
|
||||
///
|
||||
/// The RandR extension for X11 provides additional capabilities for
|
||||
/// screens and monitors, including reszing and rotating outputs. This
|
||||
/// module provides types and functions for inspecting X11 monitor
|
||||
/// configuration using the RandR extension.
|
||||
use std::ffi::CStr;
|
||||
|
||||
use x11::xlib::XGetAtomName;
|
||||
use x11::xrandr::{XRRGetMonitors, XRRMonitorInfo};
|
||||
|
||||
use super::Display;
|
||||
|
||||
/// Monitor information from the X display server
|
||||
pub struct XMonitor {
|
||||
name: String,
|
||||
monitor: XRRMonitorInfo,
|
||||
}
|
||||
|
||||
impl XMonitor {
|
||||
pub fn width(&self) -> u32 {
|
||||
self.monitor.width as u32
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u32 {
|
||||
self.monitor.height as u32
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
Some(&self.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return information about the monitors attached to an X display
|
||||
///
|
||||
/// This function returns a vector containing [`XMonitor`] structures
|
||||
/// These structures an be used to inspect monitor properties.
|
||||
pub fn get_monitors(display: &Display) -> Vec<XMonitor> {
|
||||
let mut num_monitors = 0;
|
||||
let ptr_monitors = unsafe {
|
||||
XRRGetMonitors(
|
||||
display.display,
|
||||
display.root_window(),
|
||||
0,
|
||||
&mut num_monitors,
|
||||
)
|
||||
};
|
||||
|
||||
let mut monitors = Vec::with_capacity(num_monitors as usize);
|
||||
for i in 0..num_monitors {
|
||||
let monitor = unsafe { *ptr_monitors.offset(i as isize) };
|
||||
let name = unsafe {
|
||||
CStr::from_ptr(XGetAtomName(display.display, monitor.name))
|
||||
.to_str()
|
||||
.map_or_else(|_| i.to_string(), String::from)
|
||||
};
|
||||
monitors.push(XMonitor { name, monitor });
|
||||
}
|
||||
monitors
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_monitors() {
|
||||
let dpy = Display::open().unwrap();
|
||||
let monitors = get_monitors(&dpy);
|
||||
assert!(!monitors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_monitor_name() {
|
||||
let dpy = Display::open().unwrap();
|
||||
let monitors = get_monitors(&dpy);
|
||||
let monitor = &monitors[0];
|
||||
assert!(!monitor.name().unwrap().is_empty());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue