🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
//! Sterowanie kompozytorem z powłoki (lokalny gniazdo Unix).
//!
//! Protokoły Wayland nie mają żądania „przenieś to okno na przestrzeń”, więc powłoka
//! wysyła krótkie polecenie tekstowe do `$XDG_RUNTIME_DIR/luna-control`. Gniazdo jest
//! odpytywane w głównej pętli razem z wejściem — bez dodatkowych wątków i bez
//! blokowania na kliencie (timeout odczytu).
use std::io::Read;
use std::os::fd::{AsRawFd, RawFd};
use std::os::unix::net::UnixListener;
use std::path::PathBuf;
use std::time::Duration;
use crate::state::State;
use crate::sync::LockExt;
pub struct Control {
listener: UnixListener,
}
impl Control {
/// Tworzy gniazdo `luna-control` (usuwa pozostałość po poprzedniej sesji).
pub fn spawn() -> Option<Self> {
let rt = std::env::var_os("XDG_RUNTIME_DIR")?;
let path = PathBuf::from(rt).join("luna-control");
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).ok()?;
listener.set_nonblocking(true).ok()?;
Some(Self { listener })
}
pub fn fd(&self) -> RawFd {
self.listener.as_raw_fd()
}
/// Przyjmuje i wykonuje wszystkie oczekujące polecenia.
pub fn accept(&self, state: &mut State) {
while let Ok((mut stream, _)) = self.listener.accept() {
let _ = stream.set_read_timeout(Some(Duration::from_millis(50)));
let mut buf = [0u8; 1024];
if let Ok(n) = stream.read(&mut buf) {
let text = String::from_utf8_lossy(&buf[..n]);
for line in text.lines() {
handle(state, line.trim());
}
}
}
}
}
fn handle(state: &mut State, line: &str) {
if line.is_empty() {
return;
}
let (cmd, rest) = line.split_once(' ').unwrap_or((line, ""));
match cmd {
// `move-to-workspace <indeks> <tytuł okna>`
"move-to-workspace" => {
let (ws, title) = rest.split_once(' ').unwrap_or((rest, ""));
if let Ok(ws) = ws.parse::<usize>() {
crate::seat::move_window_by_title(state, title, ws);
}
}
// `windows` — diagnostyka: lista okien w modelu kompozycji.
"windows" => {
let ws = *state.ws_current.lock_or_recover();
let wins = state.windows.lock_or_recover();
eprintln!("selene/control: okna={} ws={ws}", wins.len());
for (i, w) in wins.iter().enumerate() {
eprintln!(
"selene/control: [{i}] mapped={} min={} popup={} layer={} z={} \"{}\" {}x{} @ ({:.0},{:.0}) data={}x{}",
w.mapped,
w.minimized,
w.popup,
w.layer,
w.z,
w.title,
w.width,
w.height,
w.x,
w.y,
w.data.width,
w.data.height
);
}
}
// `screenshot` — zapis klatek wszystkich monitorów (Super+F12).
"screenshot" => {
if std::env::var("SELENE_STATS").is_ok() {
eprintln!("selene/control: zrzut ekranu");
}
state
.screenshot
.store(true, std::sync::atomic::Ordering::Relaxed);
}
// `night-light <0..100>` — siła ciepłej korekty gamma.
"night-light" => {
if let Ok(percent) = rest.parse::<u8>() {
state
.night_light
.store(percent.min(100), std::sync::atomic::Ordering::Relaxed);
if std::env::var("SELENE_STATS").is_ok() {
eprintln!("selene/control: night light {percent}%");
}
}
}
// `power-saver <0|1>` — ograniczenie tempa prezentacji.
"power-saver" => {
let on = matches!(rest.trim(), "1" | "on" | "true");
state
.power_saver
.store(on, std::sync::atomic::Ordering::Relaxed);
if std::env::var("SELENE_STATS").is_ok() {
eprintln!(
"selene/control: power saver {}",
if on { "on" } else { "off" }
);
}
}
_ => {
if std::env::var("SELENE_STATS").is_ok() {
eprintln!("selene/control: nieznane polecenie: {line}");
}
}
}
}