use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
const LOCK_FILENAME: &str = "PinReady.pid";
#[cfg(unix)]
const SOCKET_FILENAME: &str = "PinReady.sock";
static FOCUS_REQUESTED: AtomicBool = AtomicBool::new(false);
static EGUI_CTX: Mutex<Option<eframe::egui::Context>> = Mutex::new(None);
pub fn register_egui_ctx(ctx: eframe::egui::Context) {
*EGUI_CTX.lock().unwrap() = Some(ctx);
}
pub fn take_focus_request() -> bool {
FOCUS_REQUESTED.swap(false, Ordering::Relaxed)
}
pub struct PidLock {
_file: File,
path: PathBuf,
#[cfg(unix)]
socket_path: Option<PathBuf>,
}
#[derive(Debug)]
pub enum PidLockError {
OpenFailed(std::io::Error),
AlreadyRunning { path: PathBuf, pid: Option<u32> },
}
impl PidLock {
pub fn acquire_in(dir: &Path) -> Result<Self, PidLockError> {
let _ = std::fs::create_dir_all(dir);
let path = dir.join(LOCK_FILENAME);
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.map_err(PidLockError::OpenFailed)?;
match file.try_lock() {
Ok(()) => {
let _ = file.set_len(0);
let _ = file.seek(std::io::SeekFrom::Start(0));
let _ = writeln!(file, "{}", std::process::id());
#[cfg(unix)]
let socket_path = start_focus_listener(dir);
Ok(PidLock {
_file: file,
path,
#[cfg(unix)]
socket_path,
})
}
Err(_) => {
let mut content = String::new();
let pid = File::open(&path)
.ok()
.and_then(|mut f| f.read_to_string(&mut content).ok().map(|_| content))
.and_then(|s| s.trim().parse::<u32>().ok());
Err(PidLockError::AlreadyRunning { path, pid })
}
}
}
}
#[cfg(unix)]
pub fn try_notify_focus(dir: &Path) -> bool {
use std::os::unix::net::UnixStream;
let sock_path = dir.join(SOCKET_FILENAME);
match UnixStream::connect(&sock_path) {
Ok(mut stream) => {
let _ = stream.write_all(b"focus\n");
log::info!(
"Sent focus request to running instance at {}",
sock_path.display()
);
true
}
Err(e) => {
log::info!("No running instance socket at {}: {e}", sock_path.display());
false
}
}
}
#[cfg(not(unix))]
pub fn try_notify_focus(_dir: &Path) -> bool {
false
}
#[cfg(unix)]
fn start_focus_listener(dir: &Path) -> Option<PathBuf> {
use std::io::BufRead;
use std::os::unix::net::UnixListener;
let sock_path = dir.join(SOCKET_FILENAME);
let _ = std::fs::remove_file(&sock_path);
let listener = match UnixListener::bind(&sock_path) {
Ok(l) => l,
Err(e) => {
log::warn!(
"Focus-on-relaunch socket bind failed at {}: {e}. Raising existing window from another launch won't work.",
sock_path.display()
);
return None;
}
};
log::info!(
"Focus-on-relaunch socket listening at {}",
sock_path.display()
);
std::thread::Builder::new()
.name("pinready-focus-listener".into())
.spawn(move || {
for stream in listener.incoming() {
let Ok(stream) = stream else { continue };
let reader = std::io::BufReader::new(stream);
if let Some(Ok(line)) = reader.lines().next() {
log::info!("Focus request received: {line:?}");
}
FOCUS_REQUESTED.store(true, Ordering::Relaxed);
if let Some(ctx) = EGUI_CTX.lock().unwrap().as_ref() {
ctx.request_repaint();
}
}
})
.map_err(|e| log::warn!("Focus listener thread spawn failed: {e}"))
.ok();
Some(sock_path)
}
impl Drop for PidLock {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
#[cfg(unix)]
if let Some(sock) = &self.socket_path {
let _ = std::fs::remove_file(sock);
}
}
}