use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::{
Arc, Mutex,
mpsc::{self, RecvTimeoutError, SyncSender},
};
use std::thread;
use std::time::Duration;
use hyprcorrect_core::runtime;
#[derive(Default)]
pub struct ChordCaptureSlot {
sender: Mutex<Option<SyncSender<String>>>,
}
impl ChordCaptureSlot {
pub fn new() -> Arc<Self> {
Arc::new(Self::default())
}
pub fn record(&self, timeout: Duration) -> Result<String, ChordCaptureError> {
let (tx, rx) = mpsc::sync_channel::<String>(1);
{
let mut slot = self.sender.lock().expect("chord-capture slot poisoned");
*slot = Some(tx);
}
let result = match rx.recv_timeout(timeout) {
Ok(chord) => Ok(chord),
Err(RecvTimeoutError::Timeout) => Err(ChordCaptureError::Timeout),
Err(RecvTimeoutError::Disconnected) => Err(ChordCaptureError::Cancelled),
};
if result.is_err() {
self.cancel();
}
result
}
pub fn cancel(&self) {
let mut slot = self.sender.lock().expect("chord-capture slot poisoned");
*slot = None;
}
pub fn try_emit(&self, chord: String) -> bool {
let sender = {
let mut slot = self.sender.lock().expect("chord-capture slot poisoned");
slot.take()
};
match sender {
Some(tx) => {
let _ = tx.try_send(chord);
true
}
None => false,
}
}
pub fn is_armed(&self) -> bool {
self.sender
.lock()
.expect("chord-capture slot poisoned")
.is_some()
}
}
#[derive(Debug, thiserror::Error)]
pub enum ChordCaptureError {
#[error("chord-capture timed out")]
Timeout,
#[error("chord-capture was cancelled before a key arrived")]
Cancelled,
}
#[derive(Debug, thiserror::Error)]
pub enum ListenerError {
#[error("chord-capture socket bind: {0}")]
Bind(String),
}
pub const DEFAULT_RECORD_TIMEOUT: Duration = Duration::from_secs(30);
pub fn start_listener(slot: Arc<ChordCaptureSlot>) -> Result<(), ListenerError> {
let path = runtime::chord_socket_path();
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).map_err(|e| ListenerError::Bind(e.to_string()))?;
thread::spawn(move || {
for incoming in listener.incoming() {
let Ok(stream) = incoming else {
continue;
};
let slot = slot.clone();
thread::spawn(move || serve_client(stream, slot));
}
});
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("daemon not running (no chord-capture socket)")]
DaemonOffline,
#[error("chord-capture IPC: {0}")]
Io(String),
#[error("chord-capture cancelled")]
Cancelled,
#[error("daemon error: {0}")]
Daemon(String),
}
pub struct ChordRecording {
rx: mpsc::Receiver<Result<String, ClientError>>,
abort: UnixStream,
}
impl ChordRecording {
pub fn try_recv(&self) -> Result<Option<String>, ClientError> {
match self.rx.try_recv() {
Ok(result) => result.map(Some),
Err(mpsc::TryRecvError::Empty) => Ok(None),
Err(mpsc::TryRecvError::Disconnected) => Err(ClientError::Cancelled),
}
}
pub fn abort(&self) {
let _ = self.abort.shutdown(std::net::Shutdown::Both);
}
}
pub fn record_chord() -> Result<ChordRecording, ClientError> {
let path = runtime::chord_socket_path();
let mut stream = UnixStream::connect(&path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound
|| e.kind() == std::io::ErrorKind::ConnectionRefused
{
ClientError::DaemonOffline
} else {
ClientError::Io(e.to_string())
}
})?;
stream
.write_all(b"capture\n")
.map_err(|e| ClientError::Io(e.to_string()))?;
let abort = stream
.try_clone()
.map_err(|e| ClientError::Io(e.to_string()))?;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let mut reader = BufReader::new(stream);
let mut line = String::new();
let result = match reader.read_line(&mut line) {
Ok(_) => parse_reply(line.trim()),
Err(e) => Err(ClientError::Io(e.to_string())),
};
let _ = tx.send(result);
});
Ok(ChordRecording { rx, abort })
}
fn parse_reply(line: &str) -> Result<String, ClientError> {
if line.is_empty() {
return Err(ClientError::Cancelled);
}
if let Some(rest) = line.strip_prefix("err ") {
return Err(ClientError::Daemon(rest.to_string()));
}
if line == "cancel" {
return Err(ClientError::Cancelled);
}
Ok(line.to_string())
}
fn serve_client(stream: UnixStream, slot: Arc<ChordCaptureSlot>) {
let Ok(reader_stream) = stream.try_clone() else {
return;
};
let mut reader = BufReader::new(reader_stream);
let mut writer = stream;
let mut line = String::new();
if reader.read_line(&mut line).is_err() {
return;
}
let request = line.trim();
match request {
"capture" => {
let watcher_slot = slot.clone();
let watcher = thread::spawn(move || {
let mut sink = [0u8; 16];
use std::io::Read;
let mut reader = reader;
loop {
match reader.get_mut().read(&mut sink) {
Ok(0) | Err(_) => {
watcher_slot.cancel();
return;
}
Ok(_) => {
}
}
}
});
let result = slot.record(DEFAULT_RECORD_TIMEOUT);
match result {
Ok(chord) => {
let _ = writeln!(writer, "{chord}");
}
Err(ChordCaptureError::Timeout | ChordCaptureError::Cancelled) => {
let _ = writeln!(writer, "cancel");
}
}
let _ = writer.shutdown(std::net::Shutdown::Both);
let _ = watcher.join();
}
other => {
let _ = writeln!(writer, "err unknown request: {other}");
}
}
}