use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Backend {
X11,
Wayland,
}
impl std::fmt::Display for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Backend::X11 => write!(f, "X11"),
Backend::Wayland => write!(f, "Wayland"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackendInfo {
pub backend: Backend,
pub display: String,
pub socket: Option<String>, pub xdisplay: Option<String>, }
impl BackendInfo {
pub fn x11(display: &str) -> Self {
Self {
backend: Backend::X11,
display: display.to_string(),
socket: None,
xdisplay: Some(display.to_string()),
}
}
pub fn wayland(display: &str, socket: &str) -> Self {
Self {
backend: Backend::Wayland,
display: display.to_string(),
socket: Some(socket.to_string()),
xdisplay: None,
}
}
pub fn write_to_file(&self, path: &str) -> std::io::Result<()> {
let json = serde_json::to_string(self)
.map_err(std::io::Error::other)?;
fs::write(path, json)
}
pub fn read_from_file(path: &str) -> std::io::Result<Self> {
let contents = fs::read_to_string(path)?;
serde_json::from_str(&contents)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
pub fn wayland_socket_path(&self) -> Option<String> {
let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
.unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc::getuid() }));
let resolve = |s: &str| -> String {
if s.starts_with('/') {
s.to_string()
} else {
format!("{}/{}", runtime_dir, s)
}
};
self.socket
.as_deref()
.map(resolve)
.or_else(|| {
if self.backend == Backend::Wayland {
Some(resolve(&self.display))
} else {
None
}
})
}
}
pub struct BackendSelector;
impl BackendSelector {
pub const BACKEND_INFO_PATH: &'static str = "/tmp/wasma-backend-info.json";
pub fn select_interactive() -> Result<Backend, String> {
println!("\n=== Backend Seçimi ===");
println!("[1] X11");
println!("[2] Wayland");
print!("Choose Backend (1-2): ");
use std::io::{self, Write};
io::stdout().flush().ok();
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.map_err(|e| format!("Input error: {}", e))?;
match input.trim() {
"1" => Ok(Backend::X11),
"2" => Ok(Backend::Wayland),
_ => Err("Invalid selection. Please enter 1 or 2.".to_string()),
}
}
pub fn detect() -> Backend {
if let Ok(info) = BackendInfo::read_from_file(Self::BACKEND_INFO_PATH) {
return info.backend;
}
if std::env::var("WAYLAND_DISPLAY").is_ok() {
return Backend::Wayland;
}
Backend::X11
}
pub fn setup(backend: Backend) -> Result<(), String> {
match backend {
Backend::Wayland => {
if std::env::var("WAYLAND_DISPLAY").is_err()
&& !Path::new(Self::BACKEND_INFO_PATH).exists()
{
let _ = Self::try_spawn_wayland_backend();
std::thread::sleep(std::time::Duration::from_millis(400));
}
let runtime = std::env::var("XDG_RUNTIME_DIR")
.unwrap_or_else(|_| "/run/user/1000".to_string());
let socket = Self::find_valid_wayland_socket(&runtime);
std::env::set_var("WAYLAND_DISPLAY", &socket);
std::env::set_var("QT_QPA_PLATFORM", "wayland");
std::env::set_var("GDK_BACKEND", "wayland");
std::env::set_var("SDL_VIDEODRIVER", "wayland");
tracing::info!("WAYLAND_DISPLAY set to: {}", socket);
}
Backend::X11 => {
if std::env::var("DISPLAY").is_err() {
let _ = Self::try_spawn_x11_backend();
std::thread::sleep(std::time::Duration::from_millis(400));
}
if let Ok(info) = BackendInfo::read_from_file(Self::BACKEND_INFO_PATH) {
if let Some(display) = &info.xdisplay {
std::env::set_var("DISPLAY", display);
}
std::env::set_var("QT_QPA_PLATFORM", "xcb");
std::env::set_var("GDK_BACKEND", "x11");
} else if let Ok(existing) = std::env::var("DISPLAY") {
tracing::info!("Using existing DISPLAY={}", existing);
std::env::set_var("QT_QPA_PLATFORM", "xcb");
std::env::set_var("GDK_BACKEND", "x11");
} else {
std::env::set_var("DISPLAY", ":0");
std::env::set_var("QT_QPA_PLATFORM", "xcb");
std::env::set_var("GDK_BACKEND", "x11");
tracing::warn!("X11 fallback: DISPLAY=:0");
}
}
}
Ok(())
}
fn find_valid_wayland_socket(runtime: &str) -> String {
let 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(Self::BACKEND_INFO_PATH) {
if let Some(ref s) = info.socket {
if let Some(p) = check(s) { return p; }
}
if info.backend == Backend::Wayland {
if let Some(p) = check(&info.display) { return p; }
}
}
if let Ok(existing) = std::env::var("WAYLAND_DISPLAY") {
if let Some(p) = 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() {
tracing::warn!("Falling back to system Wayland socket: {}", path);
return path;
}
}
format!("{}/wayland-0", runtime)
}
fn try_spawn_wayland_backend() -> Result<(), String> {
let candidates = [
"waylandbackend",
"./target/release/waylandbackend",
"/usr/local/bin/waylandbackend",
];
for candidate in &candidates {
if let Ok(child) = std::process::Command::new(candidate)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
tracing::info!(
"Started Wayland backend from {} (pid={})",
candidate,
child.id()
);
return Ok(());
}
}
Err("Could not spawn waylandbackend; ensure it is built and in PATH".to_string())
}
fn try_spawn_x11_backend() -> Result<(), String> {
let candidates = [
"x11-backend",
"./target/release/x11-backend",
"/usr/local/bin/x11-backend",
];
for candidate in &candidates {
if let Ok(child) = std::process::Command::new(candidate)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
tracing::info!(
"Started X11 backend from {} (pid={})",
candidate,
child.id()
);
return Ok(());
}
}
Err("Could not spawn x11-backend; ensure it is built and in PATH".to_string())
}
pub fn probe_available() -> Vec<Backend> {
let mut available = Vec::new();
if std::env::var("WAYLAND_DISPLAY").is_ok()
|| Path::new(Self::BACKEND_INFO_PATH).exists()
{
available.push(Backend::Wayland);
}
if std::env::var("DISPLAY").is_ok() {
available.push(Backend::X11);
}
available
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backend_display() {
assert_eq!(Backend::X11.to_string(), "X11");
assert_eq!(Backend::Wayland.to_string(), "Wayland");
}
#[test]
fn test_backend_info_x11() {
let info = BackendInfo::x11(":0");
assert_eq!(info.backend, Backend::X11);
assert_eq!(info.display, ":0");
assert_eq!(info.xdisplay, Some(":0".to_string()));
}
#[test]
fn test_backend_info_wayland() {
let info = BackendInfo::wayland("wasma-0", "wasma-0");
assert_eq!(info.backend, Backend::Wayland);
assert_eq!(info.socket, Some("wasma-0".to_string()));
}
#[test]
fn test_write_to_file_roundtrip() {
let path = "/tmp/wasma-test-backend-info.json";
let info = BackendInfo::wayland("wasma-0", "wasma-0");
info.write_to_file(path).expect("write failed");
let read_back = BackendInfo::read_from_file(path).expect("read failed");
assert_eq!(read_back.backend, Backend::Wayland);
assert_eq!(read_back.socket, Some("wasma-0".to_string()));
std::fs::remove_file(path).ok();
}
#[test]
fn test_wayland_socket_path_absolute() {
let info = BackendInfo {
backend: Backend::Wayland,
display: "wasma-0".to_string(),
socket: Some("/run/user/1000/wasma-0".to_string()),
xdisplay: None,
};
assert_eq!(
info.wayland_socket_path(),
Some("/run/user/1000/wasma-0".to_string())
);
}
#[test]
fn test_wayland_socket_path_relative() {
std::env::set_var("XDG_RUNTIME_DIR", "/run/user/9999");
let info = BackendInfo {
backend: Backend::Wayland,
display: "wasma-0".to_string(),
socket: Some("wasma-0".to_string()),
xdisplay: None,
};
assert_eq!(
info.wayland_socket_path(),
Some("/run/user/9999/wasma-0".to_string())
);
}
}