use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Appearance {
System,
Light,
Dark,
}
impl Appearance {
fn default_system() -> Self {
Appearance::System
}
pub fn as_str(self) -> &'static str {
match self {
Appearance::System => "system",
Appearance::Light => "light",
Appearance::Dark => "dark",
}
}
}
impl std::str::FromStr for Appearance {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"system" => Ok(Appearance::System),
"light" => Ok(Appearance::Light),
"dark" => Ok(Appearance::Dark),
other => Err(format!(
"unknown appearance: {other} (expected system|light|dark)"
)),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DeviceEntry {
pub id: String,
pub name: String,
pub group: String,
pub width: u32,
pub height: u32,
pub current: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DeviceState {
pub id: String,
pub name: String,
pub group: String,
pub width: u32,
pub height: u32,
pub landscape: bool,
#[serde(default = "Appearance::default_system")]
pub appearance: Appearance,
#[serde(default = "default_capsule")]
pub capsule: bool,
}
fn default_capsule() -> bool {
true
}
pub trait DeviceController: Send + Sync {
fn list(&self) -> Result<Vec<DeviceEntry>, String>;
fn get(&self) -> Result<DeviceState, String>;
fn set(
&self,
id: Option<&str>,
landscape: Option<bool>,
appearance: Option<Appearance>,
capsule: Option<bool>,
) -> Result<DeviceState, String>;
}
static DEVICE_CONTROLLER: OnceLock<Box<dyn DeviceController>> = OnceLock::new();
pub fn register_device_controller(controller: Box<dyn DeviceController>) {
if DEVICE_CONTROLLER.set(controller).is_err() {
crate::warn!("device controller already registered; ignoring");
}
}
fn device_controller() -> Result<&'static dyn DeviceController, String> {
DEVICE_CONTROLLER
.get()
.map(|c| c.as_ref())
.ok_or_else(|| "device switching is not supported by this host".to_string())
}
pub fn device_list() -> Result<Vec<DeviceEntry>, String> {
device_controller()?.list()
}
pub fn device_get() -> Result<DeviceState, String> {
device_controller()?.get()
}
static LOGIC_CREATION_PAUSED: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
static DEVICE_CHANGE: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
pub(crate) fn logic_creation_guard()
-> Result<std::sync::MutexGuard<'static, bool>, crate::LxAppError> {
let guard = LOGIC_CREATION_PAUSED
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if *guard {
return Err(crate::LxAppError::Runtime(
"Runner device change is in progress".into(),
));
}
Ok(guard)
}
pub(crate) fn logic_creation_paused() -> bool {
*LOGIC_CREATION_PAUSED
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
struct CreationPause;
impl CreationPause {
fn begin() -> Self {
*LOGIC_CREATION_PAUSED
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = true;
Self
}
}
impl Drop for CreationPause {
fn drop(&mut self) {
*LOGIC_CREATION_PAUSED
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = false;
}
}
pub async fn device_set(
id: Option<&str>,
landscape: Option<bool>,
appearance: Option<Appearance>,
capsule: Option<bool>,
) -> Result<DeviceState, String> {
let id = id.map(str::to_owned);
crate::executor::spawn(async move { change_device(id, landscape, appearance, capsule).await })
.await
.map_err(|error| format!("device transition failed: {error}"))?
}
pub fn request_device_set(id: String, landscape: Option<bool>) {
std::mem::drop(crate::executor::spawn(async move {
if let Err(error) = device_set(Some(&id), landscape, None, None).await {
crate::error!("Runner device change failed: {error}");
}
}));
}
async fn apply_device(
id: Option<String>,
landscape: Option<bool>,
appearance: Option<Appearance>,
capsule: Option<bool>,
) -> Result<DeviceState, String> {
tokio::task::spawn_blocking(move || {
device_controller()?.set(id.as_deref(), landscape, appearance, capsule)
})
.await
.map_err(|error| format!("device controller failed: {error}"))?
}
async fn resume_apps(apps: &[std::sync::Arc<crate::LxApp>]) -> Vec<String> {
futures::future::join_all(apps.iter().map(|app| async move {
app.resume_after_device_change()
.await
.err()
.map(|error| format!("{}: {error}", app.appid))
}))
.await
.into_iter()
.flatten()
.collect()
}
async fn change_device(
id: Option<String>,
landscape: Option<bool>,
appearance: Option<Appearance>,
capsule: Option<bool>,
) -> Result<DeviceState, String> {
let _change = DEVICE_CHANGE.lock().await;
let (previous, target_group) = tokio::task::spawn_blocking({
let id = id.clone();
move || -> Result<_, String> {
let previous = device_get()?;
let group = match id {
Some(id) => {
device_list()?
.into_iter()
.find(|entry| entry.id == id)
.ok_or_else(|| format!("unknown device id: {id}"))?
.group
}
None => previous.group.clone(),
};
Ok((previous, group))
}
})
.await
.map_err(|error| error.to_string())??;
if (previous.group == "desktop") == (target_group == "desktop") {
return apply_device(id, landscape, appearance, capsule).await;
}
let pause = CreationPause::begin();
let apps = crate::lxapp::get_lxapps_manager()
.map(|manager| manager.live_logic_instances())
.unwrap_or_default();
let mut failures: Vec<String> = futures::future::join_all(apps.iter().map(|app| async move {
app.quiesce_for_device_change()
.await
.err()
.map(|error| format!("{}: {error}", app.appid))
}))
.await
.into_iter()
.flatten()
.collect();
if !failures.is_empty() {
drop(pause);
failures.extend(resume_apps(&apps).await);
return Err(failures.join("; "));
}
let applied = apply_device(id, landscape, appearance, capsule).await;
if applied.is_err()
&& let Err(error) = apply_device(
Some(previous.id),
Some(previous.landscape),
Some(previous.appearance),
Some(previous.capsule),
)
.await
{
failures.push(format!("device rollback failed: {error}"));
}
drop(pause);
failures.extend(resume_apps(&apps).await);
match applied {
Ok(state) if failures.is_empty() => Ok(state),
Ok(_) => Err(failures.join("; ")),
Err(error) => {
failures.insert(0, error);
Err(failures.join("; "))
}
}
}