use std::io::{BufRead, BufReader, Write};
use std::net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream};
use std::path::PathBuf;
use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
use crate::input::KeyCode;
pub const PORT_VAR: &str = "RENDERER_CONTROL_PORT";
#[derive(Clone, PartialEq, Debug)]
pub enum Command {
Screenshot { path: PathBuf },
Key { code: KeyCode },
Down { code: KeyCode },
Up { code: KeyCode },
Cursor { x: f32, y: f32 },
Click { at: Option<(f32, f32)> },
Press { at: Option<(f32, f32)> },
Release,
Quit,
}
pub struct Request {
pub command: Command,
reply: Sender<Result<String, String>>,
}
impl Request {
pub fn answer(self, answer: Result<String, String>) {
let _ = self.reply.send(answer);
}
pub fn command(&self) -> &Command {
&self.command
}
}
pub struct Control {
requests: Receiver<Request>,
port: u16,
}
impl Control {
pub fn from_env() -> Option<Self> {
let value = std::env::var(PORT_VAR).ok()?;
let port: u16 = match value.trim().parse() {
Ok(port) => port,
Err(_) => {
log::error!("{PORT_VAR}={value:?} is not a port number");
return None;
}
};
match Self::listen(port) {
Ok(control) => Some(control),
Err(error) => {
log::error!("could not open the control port: {error}");
None
}
}
}
pub fn listen(port: u16) -> std::io::Result<Self> {
let listener = TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, port)))?;
let port = listener.local_addr()?.port();
let (sender, requests) = channel();
std::thread::Builder::new()
.name("control".into())
.spawn(move || serve(listener, sender))?;
log::info!("control port listening on 127.0.0.1:{port}");
Ok(Self { requests, port })
}
pub fn port(&self) -> u16 {
self.port
}
pub fn poll(&self) -> Option<Request> {
match self.requests.try_recv() {
Ok(request) => Some(request),
Err(TryRecvError::Empty | TryRecvError::Disconnected) => None,
}
}
}
fn serve(listener: TcpListener, sender: Sender<Request>) {
for stream in listener.incoming() {
let Ok(mut stream) = stream else {
continue;
};
if let Err(error) = handle(&mut stream, &sender) {
log::debug!("control connection ended: {error}");
}
}
}
fn handle(stream: &mut TcpStream, sender: &Sender<Request>) -> std::io::Result<()> {
let mut lines = BufReader::new(stream.try_clone()?).lines();
while let Some(line) = lines.next().transpose()? {
let answer = match parse(&line) {
Err(problem) => Err(problem),
Ok(command) => {
let (reply, answer) = channel();
if sender.send(Request { command, reply }).is_err() {
Err("the game is gone".to_string())
} else {
answer
.recv_timeout(std::time::Duration::from_secs(10))
.unwrap_or_else(|_| Err("the game did not answer".to_string()))
}
}
};
let line = match answer {
Ok(message) => format!("ok {message}\n"),
Err(problem) => format!("error {problem}\n"),
};
stream.write_all(line.as_bytes())?;
stream.flush()?;
}
Ok(())
}
fn key_named(name: &str) -> Result<KeyCode, String> {
let wanted = name.trim();
KEYS.iter()
.find(|(spelling, _)| spelling.eq_ignore_ascii_case(wanted))
.map(|(_, code)| *code)
.ok_or_else(|| format!("no such key: {wanted}"))
}
fn point(rest: &str) -> Result<(f32, f32), String> {
let mut parts = rest.split_whitespace();
let mut next = || {
parts
.next()
.and_then(|p| p.parse::<f32>().ok())
.ok_or_else(|| format!("expected `x y` in pixels, got {rest:?}"))
};
let (x, y) = (next()?, next()?);
match parts.next() {
Some(extra) => Err(format!("only x and y, but there is also {extra:?}")),
None => Ok((x, y)),
}
}
const KEYS: [(&str, KeyCode); 23] = [
("Escape", KeyCode::Escape),
("Enter", KeyCode::Enter),
("Space", KeyCode::Space),
("Tab", KeyCode::Tab),
("ShiftLeft", KeyCode::ShiftLeft),
("Left", KeyCode::ArrowLeft),
("Right", KeyCode::ArrowRight),
("Up", KeyCode::ArrowUp),
("Down", KeyCode::ArrowDown),
("KeyV", KeyCode::KeyV),
("KeyW", KeyCode::KeyW),
("KeyA", KeyCode::KeyA),
("KeyS", KeyCode::KeyS),
("KeyD", KeyCode::KeyD),
("KeyR", KeyCode::KeyR),
("F1", KeyCode::F1),
("F2", KeyCode::F2),
("F3", KeyCode::F3),
("F4", KeyCode::F4),
("F5", KeyCode::F5),
("F9", KeyCode::F9),
("F11", KeyCode::F11),
("F12", KeyCode::F12),
];
pub fn parse(line: &str) -> Result<Command, String> {
let line = line.trim();
let (name, rest) = line.split_once(char::is_whitespace).unwrap_or((line, ""));
let rest = rest.trim();
match name {
"screenshot" if rest.is_empty() => Err("screenshot needs a path".to_string()),
"screenshot" => Ok(Command::Screenshot {
path: PathBuf::from(rest),
}),
"key" if rest.is_empty() => Err("key needs a key to press".to_string()),
"key" => key_named(rest).map(|code| Command::Key { code }),
"down" if rest.is_empty() => Err("down needs a key to hold".to_string()),
"down" => key_named(rest).map(|code| Command::Down { code }),
"up" if rest.is_empty() => Err("up needs a key to let go".to_string()),
"up" => key_named(rest).map(|code| Command::Up { code }),
"cursor" => point(rest).map(|(x, y)| Command::Cursor { x, y }),
"click" if rest.is_empty() => Ok(Command::Click { at: None }),
"click" => point(rest).map(|(x, y)| Command::Click { at: Some((x, y)) }),
"press" if rest.is_empty() => Ok(Command::Press { at: None }),
"press" => point(rest).map(|(x, y)| Command::Press { at: Some((x, y)) }),
"release" => Ok(Command::Release),
"quit" => Ok(Command::Quit),
"" => Err("say something".to_string()),
other => Err(format!("no such command: {other}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_key_is_named_the_way_winit_names_it() {
assert_eq!(parse("key F12"), Ok(Command::Key { code: KeyCode::F12 }));
assert_eq!(
parse("key KeyV"),
Ok(Command::Key {
code: KeyCode::KeyV
})
);
assert_eq!(
parse("key escape"),
Ok(Command::Key {
code: KeyCode::Escape
})
);
}
#[test]
fn a_key_can_be_held_and_let_go() {
assert_eq!(parse("down KeyW"), Ok(Command::Down { code: KeyCode::KeyW }));
assert_eq!(parse("up KeyW"), Ok(Command::Up { code: KeyCode::KeyW }));
assert!(parse("down").is_err());
assert!(parse("up Nope").is_err());
}
#[test]
fn a_key_nobody_has_is_refused_by_name() {
let refusal = parse("key Banana").unwrap_err();
assert!(refusal.contains("Banana"), "{refusal}");
assert!(parse("key").is_err(), "and a key command needs a key");
}
#[test]
fn a_screenshot_command_carries_its_path() {
assert_eq!(
parse("screenshot /tmp/board.png"),
Ok(Command::Screenshot {
path: PathBuf::from("/tmp/board.png"),
}),
);
}
#[test]
fn surrounding_whitespace_does_not_matter() {
assert_eq!(
parse(" screenshot /tmp/a b.png \r\n"),
Ok(Command::Screenshot {
path: PathBuf::from("/tmp/a b.png"),
}),
);
}
#[test]
fn a_screenshot_without_a_path_is_refused() {
assert!(parse("screenshot").is_err());
assert!(parse("screenshot ").is_err());
}
#[test]
fn quit_needs_nothing_else() {
assert_eq!(parse("quit"), Ok(Command::Quit));
assert_eq!(parse(" quit \n"), Ok(Command::Quit));
}
#[test]
fn nonsense_is_refused_by_name() {
assert_eq!(parse("wibble"), Err("no such command: wibble".to_string()));
assert!(parse("").is_err());
}
#[test]
fn a_request_can_be_answered_once() {
let (reply, answer) = channel();
let request = Request {
command: Command::Screenshot {
path: PathBuf::from("/tmp/x.png"),
},
reply,
};
request.answer(Ok("/tmp/x.png".into()));
assert_eq!(answer.recv().unwrap(), Ok("/tmp/x.png".to_string()));
}
}