use crate::backend_selector::{Backend, BackendInfo, BackendSelector};
use crate::parser::WasmaConfig;
use crate::window_multitary::WindowMultitary;
use crate::window_singularity::{WindowSingularity, SINGULARITY_LOCK};
use std::collections::HashMap;
use std::process::{Child, Command};
use std::sync::{Arc, Mutex, RwLock};
use std::sync::atomic::Ordering;
#[cfg(feature = "wayland")]
use wasma_ipc::wayland_control::WaylandControl;
#[cfg(feature = "x11")]
use x11rb::{
connection::Connection,
protocol::xproto::{
AtomEnum, ClientMessageData, ClientMessageEvent, ConfigureWindowAux,
ConnectionExt as XConnExt, EventMask, Window, CLIENT_MESSAGE_EVENT,
},
rust_connection::RustConnection,
};
#[cfg(feature = "x11")]
type X11Conn = Arc<(RustConnection, usize)>;
#[cfg(feature = "x11")]
fn acquire_x11_conn(cache: &Mutex<Option<X11Conn>>) -> Result<X11Conn, String> {
let mut guard = cache.lock().unwrap();
if let Some(ref c) = *guard {
return Ok(Arc::clone(c));
}
let (conn, screen_num) =
x11rb::connect(None).map_err(|e| format!("X11 connect failed: {e}"))?;
let arc = Arc::new((conn, screen_num));
*guard = Some(Arc::clone(&arc));
Ok(arc)
}
pub struct SpawnedApp {
pub window_id: u64,
pub app_name: String,
pub child: Child,
pub backend: Backend,
pub title: String,
pub app_id: String,
#[cfg(feature = "x11")]
pub x11_handle: Option<Window>,
#[cfg(not(feature = "x11"))]
pub x11_handle: Option<u32>,
}
pub struct WindowClient {
config: Arc<WasmaConfig>,
multitary: WindowMultitary,
singularity: WindowSingularity,
width: u32,
height: u32,
spawned: Mutex<HashMap<u64, SpawnedApp>>,
#[cfg(feature = "x11")]
x11_conn: Mutex<Option<X11Conn>>,
#[cfg(feature = "x11")]
atom_cache: RwLock<HashMap<&'static str, u32>>,
}
impl WindowClient {
pub fn new(config: WasmaConfig, width: u32, height: u32) -> Self {
let config_arc = Arc::new(config);
Self {
multitary: WindowMultitary::new((*config_arc).clone(), width, height),
singularity: WindowSingularity::new((*config_arc).clone(), width, height),
config: config_arc,
width,
height,
spawned: Mutex::new(HashMap::new()),
#[cfg(feature = "x11")]
x11_conn: Mutex::new(None),
#[cfg(feature = "x11")]
atom_cache: RwLock::new(HashMap::new()),
}
}
pub fn from_config(config: Arc<WasmaConfig>, width: u32, height: u32) -> Self {
Self {
multitary: WindowMultitary::new((*config).clone(), width, height),
singularity: WindowSingularity::new((*config).clone(), width, height),
config,
width,
height,
spawned: Mutex::new(HashMap::new()),
#[cfg(feature = "x11")]
x11_conn: Mutex::new(None),
#[cfg(feature = "x11")]
atom_cache: RwLock::new(HashMap::new()),
}
}
pub fn launch_app(&self, window_id: u64, app_name: &str, x11_bridge: bool) -> Result<(), String> {
self.launch_app_with_backend(window_id, app_name, x11_bridge, None)
}
pub fn launch_app_with_backend(
&self,
window_id: u64,
app_name: &str,
x11_bridge: bool,
preferred_backend: Option<Backend>,
) -> Result<(), String> {
let mut selected_backend = preferred_backend.unwrap_or_else(BackendSelector::detect);
BackendSelector::setup(selected_backend)?;
if selected_backend == Backend::Wayland {
let socket = Self::resolve_wayland_socket();
if !std::path::Path::new(&socket).exists() {
tracing::warn!(
"Wayland socket '{}' not found. Falling back to X11.",
socket
);
selected_backend = Backend::X11;
BackendSelector::setup(Backend::X11)?;
} else {
tracing::info!("Wayland socket confirmed: {}", socket);
}
}
let mut cmd = if app_name.contains(' ') {
let mut c = Command::new("sh");
c.arg("-c").arg(app_name);
c
} else {
Command::new(app_name)
};
self.apply_backend_env(&mut cmd, selected_backend, x11_bridge)?;
let child = match cmd.spawn() {
Ok(c) => {
tracing::info!("Direct spawn succeeded for '{app_name}'");
c
}
Err(err) => {
tracing::warn!(
"Direct spawn failed for '{app_name}': {err}. Falling back to wsdg-open"
);
let mut fallback = Command::new("wsdg-open");
fallback.arg("-a").arg(app_name);
self.apply_backend_env(&mut fallback, selected_backend, x11_bridge)?;
fallback
.spawn()
.map_err(|e| format!("wsdg-open failed for '{app_name}': {e}"))?
}
};
if SINGULARITY_LOCK.load(Ordering::SeqCst) && !self.spawned.lock().unwrap().is_empty() {
return Err(format!("Singularity mode active: cannot launch '{app_name}'"));
}
self.spawned.lock().unwrap().insert(
window_id,
SpawnedApp {
window_id,
app_name: app_name.to_string(),
child,
backend: selected_backend,
title: app_name.to_string(),
app_id: app_name.to_string(),
x11_handle: None,
},
);
tracing::info!(
"Launched '{app_name}' → window_id={window_id} backend={selected_backend} x11_bridge={x11_bridge}"
);
Ok(())
}
pub fn update_app_info(&self, window_id: u64, title: &str, app_id: &str) {
let mut spawned = self.spawned.lock().unwrap();
if let Some(app) = spawned.get_mut(&window_id) {
if !title.is_empty() { app.title = title.to_string(); }
if !app_id.is_empty() { app.app_id = app_id.to_string(); }
}
}
fn apply_backend_env(
&self,
cmd: &mut Command,
backend: Backend,
x11_bridge: bool,
) -> Result<(), String> {
match backend {
Backend::Wayland => {
let socket = Self::resolve_wayland_socket();
cmd.env("WAYLAND_DISPLAY", &socket);
cmd.env_remove("DISPLAY");
cmd.env("QT_QPA_PLATFORM", "wayland");
cmd.env("GDK_BACKEND", "wayland");
cmd.env("SDL_VIDEODRIVER", "wayland");
cmd.env("CLUTTER_BACKEND", "wayland");
tracing::info!("Wayland env applied: WAYLAND_DISPLAY={}", socket);
}
Backend::X11 => {
let x11_display = BackendInfo::read_from_file("/tmp/wasma-backend-info.json")
.ok()
.and_then(|i| i.xdisplay)
.or_else(|| std::env::var("DISPLAY").ok())
.unwrap_or_else(|| ":0".to_string());
cmd.env("DISPLAY", &x11_display);
cmd.env("QT_QPA_PLATFORM", "xcb");
cmd.env("GDK_BACKEND", "x11");
}
}
if x11_bridge {
let display = std::env::var("DISPLAY").unwrap_or_else(|_| ":0".to_string());
cmd.env("DISPLAY", display);
cmd.env("QT_QPA_PLATFORM", "xcb");
cmd.env("GDK_BACKEND", "x11");
tracing::info!("X11 bridge forced");
}
Ok(())
}
pub fn kill_app(&self, window_id: u64) -> Result<(), String> {
let mut spawned = self.spawned.lock().unwrap();
if let Some(mut app) = spawned.remove(&window_id) {
app.child
.kill()
.map_err(|e| format!("Failed to kill '{}': {e}", app.app_name))?;
println!("🗑️ Killed '{}' (window_id={window_id})", app.app_name);
Ok(())
} else {
Err(format!("No spawned app for window_id={window_id}"))
}
}
pub fn reap_dead(&self) {
let mut spawned = self.spawned.lock().unwrap();
spawned.retain(|_, app| match app.child.try_wait() {
Ok(Some(status)) => {
println!("💀 '{}' exited with {status}", app.app_name);
false
}
Ok(None) => true,
Err(e) => {
eprintln!("⚠️ try_wait error for '{}': {e}", app.app_name);
false
}
});
}
pub fn list_spawned(&self) -> Vec<(u64, String)> {
self.spawned
.lock()
.unwrap()
.iter()
.map(|(id, app)| (*id, app.app_name.clone()))
.collect()
}
#[cfg(feature = "wayland")]
fn wayland_control(&self) -> WaylandControl {
WaylandControl::new(Self::resolve_wayland_socket())
}
fn app_hints(&self, window_id: u64) -> (String, String) {
let spawned = self.spawned.lock().unwrap();
spawned.get(&window_id)
.map(|a| (a.title.clone(), a.app_id.clone()))
.unwrap_or_default()
}
#[cfg(feature = "x11")]
fn x11(&self) -> Result<X11Conn, String> {
acquire_x11_conn(&self.x11_conn)
}
#[cfg(feature = "x11")]
fn atom(&self, name: &'static str) -> Result<u32, String> {
if let Some(&v) = self.atom_cache.read().unwrap().get(name) {
return Ok(v);
}
let arc = self.x11()?;
let reply = arc
.0
.intern_atom(false, name.as_bytes())
.map_err(|e| format!("intern_atom({name}) send: {e}"))?
.reply()
.map_err(|e| format!("intern_atom({name}) reply: {e}"))?;
let atom_val = reply.atom;
self.atom_cache.write().unwrap().insert(name, atom_val);
Ok(atom_val)
}
#[cfg(feature = "x11")]
fn resolve_x11_handle(&self, window_id: u64, max_retries: u8) -> Result<Window, String> {
{
let spawned = self.spawned.lock().unwrap();
if let Some(app) = spawned.get(&window_id) {
if let Some(h) = app.x11_handle { return Ok(h); }
}
}
for attempt in 0..=max_retries {
let handle = {
let spawned = self.spawned.lock().unwrap();
let app = spawned
.get(&window_id)
.ok_or_else(|| format!("No spawned app for window_id={window_id}"))?;
self.find_x11_window(app)
};
if let Some(h) = handle {
if let Some(a) = self.spawned.lock().unwrap().get_mut(&window_id) {
a.x11_handle = Some(h);
}
return Ok(h);
}
if attempt < max_retries {
std::thread::sleep(std::time::Duration::from_millis(80));
}
}
Err(format!(
"Could not resolve X11 handle for window_id={window_id} after {max_retries} retries"
))
}
#[cfg(feature = "x11")]
fn find_x11_window(&self, app: &SpawnedApp) -> Option<Window> {
let pid = app.child.id();
if pid != 0 {
if let Some(w) = self.find_by_pid(pid) { return Some(w); }
}
self.find_by_name(&app.app_name)
}
#[cfg(feature = "x11")]
fn find_by_pid(&self, pid: u32) -> Option<Window> {
let arc = self.x11().ok()?;
let (conn, screen_num) = (&arc.0, arc.1);
let root = conn.setup().roots[screen_num].root;
let pid_atom = self.atom("_NET_WM_PID").ok()?;
self.pid_search(conn, root, pid, pid_atom)
}
#[cfg(feature = "x11")]
fn pid_search(&self, conn: &RustConnection, window: Window, pid: u32, pid_atom: u32) -> Option<Window> {
if let Some(reply) = conn
.get_property(false, window, pid_atom, AtomEnum::CARDINAL, 0, 1)
.ok()
.and_then(|c| c.reply().ok())
{
if reply.value_len > 0 && reply.value.len() >= 4 {
let win_pid = u32::from_ne_bytes(reply.value[..4].try_into().unwrap_or([0; 4]));
if win_pid == pid { return Some(window); }
}
}
if let Some(tree) = conn.query_tree(window).ok().and_then(|c| c.reply().ok()) {
for child in tree.children {
if let Some(w) = self.pid_search(conn, child, pid, pid_atom) { return Some(w); }
}
}
None
}
#[cfg(feature = "x11")]
fn find_by_name(&self, app_name: &str) -> Option<Window> {
let arc = self.x11().ok()?;
let (conn, screen_num) = (&arc.0, arc.1);
let root = conn.setup().roots[screen_num].root;
let list_atom = self.atom("_NET_CLIENT_LIST").ok()?;
let prop = conn
.get_property(false, root, list_atom, AtomEnum::ANY, 0, u32::MAX)
.ok()?.reply().ok()?;
if prop.value.len() < 4 { return None; }
let count = prop.value.len() / 4;
let win_list: Vec<Window> = (0..count)
.map(|i| {
let b = &prop.value[i * 4..i * 4 + 4];
u32::from_ne_bytes(b.try_into().unwrap_or([0; 4]))
})
.collect();
let name_atom = self.atom("_NET_WM_NAME").ok()?;
let wm_name_atom = self.atom("WM_NAME").ok()?;
let target = app_name.to_lowercase();
for w in win_list {
for atom in [name_atom, wm_name_atom] {
if let Some(r) = conn
.get_property(false, w, atom, AtomEnum::ANY, 0, u32::MAX)
.ok()
.and_then(|c| c.reply().ok())
{
if let Ok(title) = String::from_utf8(r.value) {
if title.to_lowercase().contains(&target) { return Some(w); }
}
}
}
}
None
}
#[cfg(feature = "x11")]
fn ewmh_state(&self, target: Window, action: u32, atoms: &[&'static str]) -> Result<(), String> {
let arc = self.x11()?;
let (conn, screen_num) = (&arc.0, arc.1);
let root = conn.setup().roots[screen_num].root;
let state_atom = self.atom("_NET_WM_STATE")?;
let mut data = [0u32; 5];
data[0] = action;
for (i, name) in atoms.iter().enumerate().take(2) {
data[i + 1] = self.atom(name)?;
}
let event = ClientMessageEvent {
response_type: CLIENT_MESSAGE_EVENT,
format: 32,
sequence: 0,
window: target,
type_: state_atom,
data: ClientMessageData::from(data),
};
conn.send_event(
false, root,
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
event,
).map_err(|e| format!("send_event _NET_WM_STATE: {e}"))?;
conn.flush().map_err(|e| format!("x11 flush: {e}"))?;
Ok(())
}
#[cfg(feature = "x11")]
fn ewmh_activate(&self, target: Window) -> Result<(), String> {
let arc = self.x11()?;
let (conn, screen_num) = (&arc.0, arc.1);
let root = conn.setup().roots[screen_num].root;
let active_atom = self.atom("_NET_ACTIVE_WINDOW")?;
let data = [1u32, 0, 0, 0, 0];
let event = ClientMessageEvent {
response_type: CLIENT_MESSAGE_EVENT,
format: 32,
sequence: 0,
window: target,
type_: active_atom,
data: ClientMessageData::from(data),
};
conn.send_event(
false, root,
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
event,
).map_err(|e| format!("send_event _NET_ACTIVE_WINDOW: {e}"))?;
conn.flush().map_err(|e| format!("x11 flush: {e}"))?;
Ok(())
}
#[cfg(feature = "x11")]
fn icccm_iconify(&self, target: Window) -> Result<(), String> {
let arc = self.x11()?;
let (conn, screen_num) = (&arc.0, arc.1);
let root = conn.setup().roots[screen_num].root;
let change_state_atom = self.atom("WM_CHANGE_STATE")?;
let data = [3u32, 0, 0, 0, 0];
let event = ClientMessageEvent {
response_type: CLIENT_MESSAGE_EVENT,
format: 32,
sequence: 0,
window: target,
type_: change_state_atom,
data: ClientMessageData::from(data),
};
conn.send_event(
false, root,
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
event,
).map_err(|e| format!("send_event WM_CHANGE_STATE (iconify): {e}"))?;
conn.flush().map_err(|e| format!("x11 flush: {e}"))?;
Ok(())
}
pub fn set_focus(&self, window_id: u64) -> Result<(), String> {
match self.backend_of(window_id)? {
Backend::X11 => {
#[cfg(feature = "x11")]
{
let h = self.resolve_x11_handle(window_id, 5)?;
self.ewmh_activate(h)?;
tracing::info!("✓ Focused X11 window_id={window_id}");
Ok(())
}
#[cfg(not(feature = "x11"))]
Err("X11 support not compiled".into())
}
Backend::Wayland => {
#[cfg(feature = "wayland")]
{
let (title, app_id) = self.app_hints(window_id);
let ctrl = self.wayland_control();
ctrl.activate(window_id, &title, &app_id, None)?;
tracing::info!("✓ Focused Wayland window_id={window_id}");
Ok(())
}
#[cfg(not(feature = "wayland"))]
{
tracing::debug!("set_focus: wayland feature disabled");
Ok(())
}
}
}
}
pub fn minimize(&self, window_id: u64) -> Result<(), String> {
match self.backend_of(window_id)? {
Backend::X11 => {
#[cfg(feature = "x11")]
{
let h = self.resolve_x11_handle(window_id, 5)?;
self.icccm_iconify(h)?;
tracing::info!("✓ Minimized X11 window_id={window_id}");
Ok(())
}
#[cfg(not(feature = "x11"))]
Err("X11 support not compiled".into())
}
Backend::Wayland => {
#[cfg(feature = "wayland")]
{
let (title, app_id) = self.app_hints(window_id);
let ctrl = self.wayland_control();
ctrl.set_minimized(window_id, &title, &app_id)?;
tracing::info!("✓ Minimized Wayland window_id={window_id}");
Ok(())
}
#[cfg(not(feature = "wayland"))]
{
tracing::debug!("minimize: wayland feature disabled");
Ok(())
}
}
}
}
pub fn maximize(&self, window_id: u64) -> Result<(), String> {
match self.backend_of(window_id)? {
Backend::X11 => {
#[cfg(feature = "x11")]
{
let h = self.resolve_x11_handle(window_id, 5)?;
self.ewmh_state(
h, 1,
&["_NET_WM_STATE_MAXIMIZED_HORZ", "_NET_WM_STATE_MAXIMIZED_VERT"],
)?;
tracing::info!("✓ Maximized X11 window_id={window_id}");
Ok(())
}
#[cfg(not(feature = "x11"))]
Err("X11 support not compiled".into())
}
Backend::Wayland => {
#[cfg(feature = "wayland")]
{
let (title, app_id) = self.app_hints(window_id);
let ctrl = self.wayland_control();
ctrl.set_maximized(window_id, &title, &app_id)?;
tracing::info!("✓ Maximized Wayland window_id={window_id}");
Ok(())
}
#[cfg(not(feature = "wayland"))]
{
tracing::debug!("maximize: wayland feature disabled");
Ok(())
}
}
}
}
pub fn toggle_fullscreen(&self, window_id: u64) -> Result<(), String> {
match self.backend_of(window_id)? {
Backend::X11 => {
#[cfg(feature = "x11")]
{
let h = self.resolve_x11_handle(window_id, 5)?;
self.ewmh_state(h, 2, &["_NET_WM_STATE_FULLSCREEN"])?;
tracing::info!("✓ Toggled fullscreen X11 window_id={window_id}");
Ok(())
}
#[cfg(not(feature = "x11"))]
Err("X11 support not compiled".into())
}
Backend::Wayland => {
#[cfg(feature = "wayland")]
{
let (title, app_id) = self.app_hints(window_id);
let ctrl = self.wayland_control();
ctrl.set_fullscreen(window_id, &title, &app_id)?;
tracing::info!("✓ Toggled fullscreen Wayland window_id={window_id}");
Ok(())
}
#[cfg(not(feature = "wayland"))]
{
tracing::debug!("toggle_fullscreen: wayland feature disabled");
Ok(())
}
}
}
}
pub fn hide(&self, window_id: u64) -> Result<(), String> {
match self.backend_of(window_id)? {
Backend::X11 => {
#[cfg(feature = "x11")]
{
let h = self.resolve_x11_handle(window_id, 5)?;
self.ewmh_state(h, 1, &["_NET_WM_STATE_HIDDEN"])?;
tracing::info!("✓ Hidden X11 window_id={window_id}");
Ok(())
}
#[cfg(not(feature = "x11"))]
Err("X11 support not compiled".into())
}
Backend::Wayland => {
#[cfg(feature = "wayland")]
{
let (title, app_id) = self.app_hints(window_id);
let ctrl = self.wayland_control();
ctrl.set_minimized(window_id, &title, &app_id)?;
tracing::info!("✓ Hidden (minimized) Wayland window_id={window_id}");
Ok(())
}
#[cfg(not(feature = "wayland"))]
{
tracing::debug!("hide: wayland feature disabled");
Ok(())
}
}
}
}
pub fn try_embed_app(
&self,
window_id: u64,
parent_window: u32,
width: u32,
height: u32,
retries: u8,
) -> Result<(), String> {
#[cfg(feature = "x11")]
{
let h = self.resolve_x11_handle(window_id, retries)?;
let arc = self.x11()?;
let conn = &arc.0;
conn.reparent_window(h, parent_window as Window, 0, 0)
.map_err(|e| format!("reparent_window: {e}"))?;
conn.configure_window(
h,
&ConfigureWindowAux::new().width(width).height(height),
)
.map_err(|e| format!("configure_window: {e}"))?;
conn.map_window(h).map_err(|e| format!("map_window: {e}"))?;
conn.flush().map_err(|e| format!("x11 flush: {e}"))?;
if let Some(a) = self.spawned.lock().unwrap().get_mut(&window_id) {
a.x11_handle = Some(h);
}
tracing::info!("✓ Embedded X11 window_id={window_id} into parent={parent_window}");
Ok(())
}
#[cfg(not(feature = "x11"))]
Err("X11 support not compiled".into())
}
fn resolve_wayland_socket() -> String {
let runtime = std::env::var("XDG_RUNTIME_DIR")
.unwrap_or_else(|_| format!("/run/user/{}", std::process::id()));
let resolve_and_check = |s: &str| -> Option<String> {
let path = if s.starts_with('/') {
s.to_string()
} else {
format!("{}/{}", runtime, s)
};
if std::path::Path::new(&path).exists() { Some(path) } else { None }
};
if let Ok(info) = BackendInfo::read_from_file("/tmp/wasma-backend-info.json") {
if let Some(ref s) = info.socket {
if let Some(p) = resolve_and_check(s) { return p; }
}
if info.backend == Backend::Wayland {
if let Some(p) = resolve_and_check(&info.display) { return p; }
}
}
if let Ok(existing) = std::env::var("WAYLAND_DISPLAY") {
if let Some(p) = resolve_and_check(&existing) { return p; }
}
if let Ok(entries) = std::fs::read_dir(&runtime) {
#[cfg(unix)]
use std::os::unix::fs::FileTypeExt;
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with("wasma") {
#[cfg(unix)]
{
if entry.file_type().map(|t| t.is_socket()).unwrap_or(false) {
return entry.path().to_string_lossy().into_owned();
}
}
#[cfg(not(unix))]
{ return entry.path().to_string_lossy().into_owned(); }
}
}
}
for name in &["wayland-1", "wayland-0"] {
let path = format!("{}/{}", runtime, name);
if std::path::Path::new(&path).exists() { return path; }
}
format!("{}/wayland-0", runtime)
}
fn backend_of(&self, window_id: u64) -> Result<Backend, String> {
self.spawned
.lock()
.unwrap()
.get(&window_id)
.map(|a| a.backend)
.ok_or_else(|| format!("No spawned app for window_id={window_id}"))
}
pub fn resize(&mut self, new_width: u32, new_height: u32) {
self.width = new_width;
self.height = new_height;
self.multitary.update_resolution(new_width, new_height);
}
pub fn get_dimensions(&self) -> (u32, u32) { (self.width, self.height) }
pub fn get_config(&self) -> &WasmaConfig { &self.config }
pub fn enter_singularity(&mut self, stream_id: u8) {
self.singularity.enter_singularity_mode(stream_id);
}
pub fn exit_singularity(&mut self) {
self.singularity.exit_singularity_mode();
}
pub fn is_singularity_active(&self) -> bool {
SINGULARITY_LOCK.load(Ordering::SeqCst)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::ConfigParser;
fn make_client() -> WindowClient {
let parser = ConfigParser::new(None);
let config = parser.parse(&parser.generate_default_config()).unwrap();
WindowClient::new(config, 1920, 1080)
}
#[test]
fn test_window_client_creation() {
let client = make_client();
assert_eq!(client.get_dimensions(), (1920, 1080));
}
#[test]
fn test_singularity_toggle() {
let mut client = make_client();
assert!(!client.is_singularity_active());
client.enter_singularity(0);
assert!(client.is_singularity_active());
client.exit_singularity();
assert!(!client.is_singularity_active());
}
#[test]
fn test_list_spawned_empty() {
let client = make_client();
assert!(client.list_spawned().is_empty());
}
#[test]
fn test_backend_of_missing() {
let client = make_client();
assert!(client.backend_of(9999).is_err());
}
#[test]
fn test_update_app_info() {
let client = make_client();
client.update_app_info(9999, "firefox", "org.firefox");
}
}