🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
//! Skryptowane wejście do testów (`SELENE_INPUT_SCRIPT`).
//!
//! W izolowanej sesji testowej nie ma fizycznej myszy ani nadrzędnego kompozytora,
//! więc nie da się sprawdzić routingu kliknięć „na żywo”. Ten moduł czyta polecenia
//! z pliku (może być zwykły plik, do którego dopisujemy) i podaje je do
//! [`crate::seat::handle_input`] dokładnie tak, jak zrobiłby to evdev.
//!
//! Format — jedna komenda na linię:
//!
//! ```text
//! move X Y
//! down X Y
//! up X Y
//! click X Y
//! rclick X Y # prawy przycisk (menu kontekstowe taskbara)
//! keydown KOD # KOD = fizyczny kod evdev (np. 42 = Shift, 3 = "2")
//! keyup KOD
//! ```
//!
//! Włączenie: `SELENE_INPUT_SCRIPT=/ścieżka/do/pliku selene`. Bez zmiennej moduł
//! nic nie robi i nie wpływa na normalną sesję.
use std::path::PathBuf;
use libluna_core::{ButtonState, Event, KeyEvent, Modifiers, Point, PointerButton, PointerEvent};
use wayland_server::DisplayHandle;
use crate::state::State;
/// Czytnik skryptu wejścia (śledzi pozycję w pliku, więc dopisywanie działa).
pub struct ScriptInput {
path: PathBuf,
offset: u64,
/// Ta sama mapa klawiatury co w evdev — pozwala odtworzyć keysym/tekst klawisza.
#[allow(dead_code)]
keymap: Option<xkbcommon::xkb::Keymap>,
xkb: Option<xkbcommon::xkb::State>,
}
impl ScriptInput {
/// `Some`, gdy włączono `SELENE_INPUT_SCRIPT`.
pub fn open() -> Option<Self> {
let path = std::env::var_os("SELENE_INPUT_SCRIPT")?;
let (keymap, xkb) = match xkbcommon::xkb::Keymap::new_from_string(
&xkbcommon::xkb::Context::new(xkbcommon::xkb::CONTEXT_NO_FLAGS),
crate::seat::keymap_text(),
xkbcommon::xkb::KEYMAP_FORMAT_TEXT_V1,
xkbcommon::xkb::KEYMAP_COMPILE_NO_FLAGS,
) {
Some(km) => {
let state = xkbcommon::xkb::State::new(&km);
(Some(km), Some(state))
}
None => (None, None),
};
Some(Self {
path: PathBuf::from(path),
offset: 0,
keymap,
xkb,
})
}
/// Przetwarza nowe linie dopisane do pliku.
pub fn poll(&mut self, state: &mut State, handle: &DisplayHandle) {
let Ok(text) = std::fs::read_to_string(&self.path) else {
return;
};
if text.len() as u64 == self.offset {
// Brak nowych danych — to normalny stan przy każdym `poll`.
return;
}
if (text.len() as u64) < self.offset {
// Plik skrócony (nowy test) — zacznij od początku.
self.offset = 0;
}
let start = self.offset as usize;
if start > text.len() || !text.is_char_boundary(start) {
self.offset = 0;
return;
}
let chunk = text[start..].to_owned();
self.offset = text.len() as u64;
for line in chunk.lines() {
self.apply(line, state, handle);
}
}
fn apply(&mut self, line: &str, state: &mut State, handle: &DisplayHandle) {
let mut parts = line.split_whitespace();
let Some(command) = parts.next() else {
return;
};
match command {
"keydown" | "keyup" => {
if let Some(code) = parts.next().and_then(|c| c.parse::<u16>().ok()) {
self.key(code, command == "keydown", state, handle);
}
return;
}
_ => {}
}
let point = parse_point(&mut parts);
match (command, point) {
("move", Some(p)) => send_move(state, handle, p),
("down", Some(p)) => {
send_button(state, handle, p, PointerButton::Left, ButtonState::Pressed)
}
("up", Some(p)) => {
send_button(state, handle, p, PointerButton::Left, ButtonState::Released)
}
("rdown", Some(p)) => {
send_button(state, handle, p, PointerButton::Right, ButtonState::Pressed)
}
("rup", Some(p)) => send_button(
state,
handle,
p,
PointerButton::Right,
ButtonState::Released,
),
("click", Some(p)) => {
send_move(state, handle, p);
send_button(state, handle, p, PointerButton::Left, ButtonState::Pressed);
send_button(state, handle, p, PointerButton::Left, ButtonState::Released);
}
("rclick", Some(p)) => {
send_move(state, handle, p);
send_button(state, handle, p, PointerButton::Right, ButtonState::Pressed);
send_button(
state,
handle,
p,
PointerButton::Right,
ButtonState::Released,
);
}
_ => {}
}
}
/// Wysyła zdarzenie klawisza z keysymem/tekstem obliczonym przez xkb.
fn key(&mut self, code: u16, pressed: bool, state: &mut State, handle: &DisplayHandle) {
let Some(xkb) = self.xkb.as_mut() else {
return;
};
let kc = xkbcommon::xkb::Keycode::new(u32::from(code) + 8);
let dir = if pressed {
xkbcommon::xkb::KeyDirection::Down
} else {
xkbcommon::xkb::KeyDirection::Up
};
xkb.update_key(kc, dir);
let keysym = xkb.key_get_one_sym(kc).raw();
let text = if pressed {
let s = xkb.key_get_utf8(kc);
if s.is_empty() {
None
} else {
Some(s)
}
} else {
None
};
let event = Event::Key(KeyEvent {
keysym,
keycode: u32::from(code),
text,
state: if pressed {
ButtonState::Pressed
} else {
ButtonState::Released
},
modifiers: Modifiers::NONE,
repeat: false,
});
crate::seat::handle_input(state, handle, &event);
}
}
fn parse_point(parts: &mut std::str::SplitWhitespace<'_>) -> Option<Point> {
let x: f64 = parts.next()?.parse().ok()?;
let y: f64 = parts.next()?.parse().ok()?;
Some(Point::new(x, y))
}
fn send_move(state: &mut State, handle: &DisplayHandle, position: Point) {
let event = Event::PointerMotion {
position,
modifiers: Modifiers::NONE,
};
crate::seat::handle_input(state, handle, &event);
}
fn send_button(
state: &mut State,
handle: &DisplayHandle,
position: Point,
button: PointerButton,
button_state: ButtonState,
) {
let event = Event::PointerButton(PointerEvent {
position,
button,
state: button_state,
modifiers: Modifiers::NONE,
});
crate::seat::handle_input(state, handle, &event);
}