🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
//! Selene — kompozytor Wayland środowiska Luna.
//!
//! Świadomie **bez smithay**: protokół obsługujemy własną warstwą na `wayland-server`
//! (rdzeń + xdg-shell + output + seat), a pętla zdarzeń to bezpośredni `poll` po gnieździe
//! nasłuchu, deskryptorze backendu i magistrali wejścia. Wyjście (nested) i kompozycja
//! idą przez `libluna-platform` + `libluna-render`.
mod compositor;
mod control;
mod data;
mod decoration;
mod drm;
mod evdev;
mod foreign_toplevel;
mod input;
mod layer;
mod monitors;
mod output;
mod popup;
mod seat;
mod shell;
mod shm;
mod state;
mod subsurface;
mod sync;
mod test_input;
mod workspace;
mod xwayland;
mod xwm;
use std::error::Error;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Arc;
use libluna_core::Event;
use wayland_protocols::ext::workspace::v1::server::ext_workspace_manager_v1::ExtWorkspaceManagerV1;
use wayland_protocols::xdg::decoration::zv1::server::zxdg_decoration_manager_v1;
use wayland_protocols::xdg::shell::server::xdg_wm_base;
use wayland_protocols::xwayland::shell::v1::server::xwayland_shell_v1::XwaylandShellV1;
use wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
use wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_shell_v1;
use wayland_server::protocol::{
wl_compositor, wl_data_device_manager, wl_output, wl_seat, wl_shm, wl_subcompositor,
};
use wayland_server::{Display, ListeningSocket};
use crate::sync::LockExt;
use state::State;
fn main() -> Result<(), Box<dyn Error>> {
// Wielojęzyczność: lokalizacja z otoczenia + katalogi `i18n/` doładowane w runtime.
libluna_i18n::set_locale(libluna_i18n::Locale::from_env());
libluna_i18n::load_catalogs_from_dirs();
// Krótszy, jednoznaczny log panik — bez zasypywania sesji pełnym backtrace'em.
// Paniki klientów są łapane niżej (`catch_unwind`), więc sesja żyje dalej.
// Z `RUST_BACKTRACE=1` dopisujemy ślad — bez niego komunikat „objects from wrong
// client” nie mówi, która ścieżka wysłała zdarzenie.
std::panic::set_hook(Box::new(|info| {
eprintln!("Selene panic: {info}");
let bt = std::env::var("RUST_BACKTRACE").unwrap_or_default();
if !bt.is_empty() && bt != "0" {
eprintln!("{}", std::backtrace::Backtrace::force_capture());
}
}));
let mut display: Display<State> = Display::new()?;
let mut handle = display.handle();
// Diagnostyka DRM: `SELENE_DRM_PROBE=1` wypisuje karty, konektory i tryby EDID
// (bez zmiany trybu — nie trzeba być DRM masterem).
if std::env::var("SELENE_DRM_PROBE").as_deref() == Ok("1") {
print!("{}", drm::probe_report());
return Ok(());
}
let mut state = State::default();
// Diagnostyka: skryptowane wejście (`SELENE_INPUT_SCRIPT`) — używane w testach
// izolowanej sesji, aby odtworzyć kliknięcia bez fizycznej myszy.
let mut script_input = test_input::ScriptInput::open();
// Wykrywanie monitorów: DRM/KMS, a gdy niedostępne — pojedyncze wyjście nested.
if let Some(outs) = drm::discover() {
if !outs.is_empty() {
println!(
"{}: {}",
libluna_i18n::tr("Detected outputs"),
outs.iter()
.map(|o| format!("{} {}x{}@{}", o.name, o.width, o.height, o.refresh_hz))
.collect::<Vec<_>>()
.join(", ")
);
*state.outputs.lock_or_recover() = outs;
}
}
let output_count = state.outputs.lock_or_recover().len();
let ws_count = state.ws_count;
// Globalne interfejsy protokołu.
handle.create_global::<State, wl_compositor::WlCompositor, ()>(4, ());
handle.create_global::<State, wl_subcompositor::WlSubcompositor, ()>(1, ());
handle.create_global::<State, wl_shm::WlShm, ()>(1, ());
// Jeden global `wl_output` na monitor.
for index in 0..output_count {
handle.create_global::<State, wl_output::WlOutput, usize>(4, index);
}
state.output_globals = output_count;
handle.create_global::<State, xdg_wm_base::XdgWmBase, ()>(1, ());
handle.create_global::<State, zxdg_decoration_manager_v1::ZxdgDecorationManagerV1, ()>(1, ());
handle.create_global::<State, zwlr_layer_shell_v1::ZwlrLayerShellV1, ()>(4, ());
handle.create_global::<State, wl_data_device_manager::WlDataDeviceManager, ()>(3, ());
// Wersja 4 to minimum dla `wl_keyboard.repeat_info`; wersja 1 kończyła klienta
// twardym przerwaniem, gdy wysyłaliśmy zdarzenie ponad zadeklarowaną wersję obiektu.
handle.create_global::<State, wl_seat::WlSeat, ()>(5, ());
handle.create_global::<State, ZwlrForeignToplevelManagerV1, ()>(3, ());
handle.create_global::<State, ExtWorkspaceManagerV1, ()>(1, ());
// X11 (XWayland): bez tego globalu Xwayland nie przypisze okien X11 do powierzchni.
handle.create_global::<State, XwaylandShellV1, ()>(1, ());
// Gniazdo nasłuchu: `wayland-0`, `wayland-1`, …
ensure_runtime_dir()?;
let socket =
ListeningSocket::bind_auto("wayland", 0..33).map_err(|e| format!("bind socket: {e:?}"))?;
let socket_name = socket
.socket_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
println!(
"{} ({socket_name})",
libluna_i18n::tr("Selene compositor started")
);
// Marker gotowości dla menedżera sesji: nazwa gniazda, na którym **naprawdę**
// słuchamy. Bez tego sesja musi zgadywać `wayland-0` — a jeśli po poprzedniej
// sesji został martwy plik gniazda, połączyłaby się z nikim.
if let Ok(rt) = std::env::var("XDG_RUNTIME_DIR") {
let path = std::path::Path::new(&rt).join("luna-wayland");
let _ = std::fs::write(&path, socket_name.as_bytes());
// Liczba monitorów dla menedżera sesji: tyle instancji powłoki uruchomi —
// jedną na każdy wyświetlacz (każda wiąże swoje `wl_output`).
let outs = std::path::Path::new(&rt).join("luna-outputs");
let _ = std::fs::write(&outs, output_count.to_string());
}
// Aplikacje X11: uruchamiamy Xwayland i zapisujemy numer ekranu dla menedżera sesji.
// Brak Xwaylanda nie może przewrócić sesji Wayland — dlatego wszystko jest opcjonalne.
if std::env::var("SELENE_NO_XWAYLAND").is_err() {
// Marker z poprzedniej sesji mógłby dać sesji `DISPLAY` na martwy ekran.
xwayland::clear_display_marker();
match xwayland::spawn(&socket_name) {
Some(display) => {
// XWM startuje w tle (Xwayland potrzebuje chwili, zanim zacznie
// przyjmować połączenia) i sam rejestruje się, gdy będzie gotowy.
xwm::spawn(display);
}
None => {
if std::env::var("SELENE_STATS").is_ok() {
eprintln!(
"selene/xwayland: nie udało się uruchomić Xwayland (aplikacje X11 nie ruszą)"
);
}
}
}
}
// Magistrala wejścia (wątek wyjścia → wątek protokołu) z parą gniazd do wybudzania.
let (events, mut wake_read, wake_write) = input::channel();
// Wyjście: realne DRM/KMS (gdy `SELENE_BACKEND=drm`) lub nested na nadrzędnym kompozytorze.
if std::env::var("SELENE_BACKEND").as_deref() == Ok("drm") {
output::spawn_drm(
Arc::clone(&state.windows),
Arc::clone(&state.cursor),
Arc::clone(&state.ws_current),
Arc::clone(&state.overview),
Arc::clone(&state.screenshot),
Arc::clone(&state.night_light),
Arc::clone(&state.power_saver),
Arc::clone(&state.outputs),
ws_count,
);
} else {
output::spawn(
Arc::clone(&state.windows),
Arc::clone(&state.outputs),
Arc::clone(&events),
wake_write,
Arc::clone(&state.ws_current),
Arc::clone(&state.overview),
Arc::clone(&state.screenshot),
ws_count,
1280,
800,
);
}
let listener_fd = socket.as_raw_fd();
// Wejście evdev (dla trybu DRM, gdzie nie ma nadrzędnego kompozytora).
let mut evdev_input = if std::env::var("SELENE_INPUT").as_deref() == Ok("evdev") {
match evdev::EvdevInput::open() {
Some(input) => {
println!("{}", libluna_i18n::tr("evdev input enabled"));
Some(input)
}
None => {
eprintln!("{}", libluna_i18n::tr("evdev input unavailable"));
None
}
}
} else {
None
};
// Blokady (NumLock/CapsLock) mogły być włączone jeszcze przed startem sesji —
// bez tego numpad dawał nawigację zamiast cyfr, dopóki nie przełączysz NumLock.
if let Some(input) = &evdev_input {
let (num_lock, caps_lock) = input.locks();
state.num_lock = num_lock;
state.caps_lock = caps_lock;
}
// Sterowanie z powłoki (PPM w taskbarze → „przenieś na przestrzeń”).
let control = control::Control::spawn();
// Diagnostyka (SELENE_STATS=1): częstość aktualizacji kursora (tempo wejścia evdev).
let stats = std::env::var("SELENE_STATS").is_ok();
let mut cursor_updates = 0u64;
let mut stats_report = std::time::Instant::now();
loop {
// Skryptowane wejście (testy izolowanej sesji).
if let Some(script) = script_input.as_mut() {
script.poll(&mut state, &handle);
}
let backend_fd = display.backend().poll_fd().as_raw_fd();
let wake_fd = wake_read.as_raw_fd();
let listener = unsafe { BorrowedFd::borrow_raw(listener_fd) };
let backend = unsafe { BorrowedFd::borrow_raw(backend_fd) };
let wake = unsafe { BorrowedFd::borrow_raw(wake_fd) };
let input_raw = evdev_input.as_ref().map(|e| e.fds()).unwrap_or_default();
// SAFETY: deskryptory należą do `evdev_input`, który żyje przez całą iterację.
let input_fds: Vec<BorrowedFd> = input_raw
.iter()
.map(|&r| unsafe { BorrowedFd::borrow_raw(r) })
.collect();
let mut fds = vec![
rustix::event::PollFd::new(&listener, rustix::event::PollFlags::IN),
rustix::event::PollFd::new(&backend, rustix::event::PollFlags::IN),
rustix::event::PollFd::new(&wake, rustix::event::PollFlags::IN),
];
for fd in &input_fds {
fds.push(rustix::event::PollFd::new(fd, rustix::event::PollFlags::IN));
}
// Gniazdo sterowania powłoki (opcjonalne — brak nie może zatrzymać sesji).
let control_fd = control
.as_ref()
.map(|c| unsafe { BorrowedFd::borrow_raw(c.fd()) });
if let Some(fd) = &control_fd {
fds.push(rustix::event::PollFd::new(fd, rustix::event::PollFlags::IN));
}
// Przy skrypcie wejścia budzimy się co ~16 ms, żeby zauważyć dopisane polecenia.
let poll_timeout = if script_input.is_some() { 16 } else { -1 };
rustix::event::poll(&mut fds, poll_timeout)?;
// Sterowanie z powłoki (np. przeniesienie okna na inną przestrzeń).
if let (Some(c), Some(_)) = (&control, &control_fd) {
let idx = 3 + input_fds.len();
if fds[idx].revents().contains(rustix::event::PollFlags::IN) {
c.accept(&mut state);
}
}
// Wejście evdev → trasowanie (jak z magistrali).
if input_fds
.iter()
.enumerate()
.any(|(i, _)| fds[3 + i].revents().contains(rustix::event::PollFlags::IN))
{
if let Some(input) = evdev_input.as_mut() {
let bounds = {
let outs = state.outputs.lock_or_recover();
if outs.is_empty() {
(1280.0, 800.0)
} else {
let total_w = outs
.iter()
.map(|o| (o.x + o.width) as f64)
.fold(0.0, f64::max);
let total_h = outs
.iter()
.map(|o| (o.y + o.height) as f64)
.fold(0.0, f64::max);
(total_w, total_h)
}
};
let batch = input.read_events(bounds);
*state.cursor.lock_or_recover() = Some(input.cursor());
cursor_updates += 1;
for event in &batch {
seat::handle_input(&mut state, &handle, event);
}
}
}
// Nowy klient.
if fds[0].revents().contains(rustix::event::PollFlags::IN) {
while let Ok(Some(stream)) = socket.accept() {
match handle.insert_client(stream, Arc::new(())) {
Ok(_) => state.clients += 1,
Err(e) => eprintln!("client rejected: {e}"),
}
}
}
// Wejście z magistrali → trasowanie do klientów.
if fds[2].revents().contains(rustix::event::PollFlags::IN) {
input::drain_wake(&mut wake_read);
let batch: Vec<Event> = {
let mut queue = events.lock_or_recover();
std::mem::take(&mut *queue)
};
for event in &batch {
seat::handle_input(&mut state, &handle, event);
}
}
// Odporność: panika albo błąd w obsłudze pojedynczego klienta nie może zabić
// całej sesji — łapiemy panikę i kontynuujemy pętlę.
match catch_unwind(AssertUnwindSafe(|| display.dispatch_clients(&mut state))) {
Ok(Ok(_)) => {}
Ok(Err(e)) => return Err(e.into()),
Err(_) => eprintln!("{}", libluna_i18n::tr("client request panicked; ignoring")),
}
match catch_unwind(AssertUnwindSafe(|| display.flush_clients())) {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(e.into()),
Err(_) => eprintln!("{}", libluna_i18n::tr("client request panicked; ignoring")),
}
// X11: tytuły okien i życzenia stanu od klientów (XWM działa w osobnym wątku).
xwm::sync(&mut state);
// Faktyczny modeset DRM mógł poprawić rozmiar/odświeżanie — wyślij klientom
// świeże `wl_output` (inaczej panel i okna zostają przy wczesnym probe).
monitors::refresh_bound(&mut state);
// Gdy DRM wykrył więcej monitorów niż wczesny probe, dołóż brakujące globale.
let want_outputs = state.outputs.lock_or_recover().len();
while state.output_globals < want_outputs {
handle.create_global::<State, wl_output::WlOutput, usize>(4, state.output_globals);
state.output_globals += 1;
}
if stats && stats_report.elapsed().as_secs_f64() >= 2.0 {
let secs = stats_report.elapsed().as_secs_f64();
let windows = state.windows.lock_or_recover().len();
eprintln!(
"selene/input: {:.0} aktualizacji kursora/s | klienci={} okna={} | x11(wm)={}",
cursor_updates as f64 / secs,
state.clients,
windows,
xwm::global().map(|x| x.is_wm()).unwrap_or(false)
);
cursor_updates = 0;
stats_report = std::time::Instant::now();
}
}
}
/// Zapewnia, że `XDG_RUNTIME_DIR` jest ustawiony, zanim zwiążemy gniazdo Wayland.
///
/// Wspólna logika (wybór `/run/user/<uid>` albo prywatnego katalogu pod `$TMPDIR`)
/// mieszka w [`libluna_core::runtime`] — korzystają z niej też klienci (m.in. `luna-shell`),
/// żeby trafić na to samo gniazdo. Tu tylko raportujemy wybrany katalog.
fn ensure_runtime_dir() -> Result<(), Box<dyn Error>> {
if let Some(dir) = libluna_core::runtime::ensure_runtime_dir()? {
println!(
"{} {}",
libluna_i18n::tr("XDG_RUNTIME_DIR not set, using"),
dir.display()
);
}
Ok(())
}