#![warn(
clippy::complexity,
clippy::correctness,
clippy::style,
future_incompatible,
missing_debug_implementations,
missing_docs,
rust_2018_idioms,
rustdoc::all,
clippy::undocumented_unsafe_blocks
)]
use ninep::{
sansio::server::socket_dir,
sync::client::{ReadLineIter, UnixClient},
};
use std::{env, fs, io, io::Write, os::unix::net::UnixStream, str::FromStr};
mod event;
pub use ad_event::Source;
pub use event::{EventFilter, Outcome};
#[derive(Debug, Clone)]
pub struct Client {
inner: UnixClient,
ns: String,
}
impl Client {
pub fn new() -> io::Result<Self> {
let ns = match env::var("AD_PID") {
Ok(pid) => format!("ad-{pid}"),
Err(_) => "ad".to_string(),
};
Ok(Self {
inner: UnixClient::new_unix(&ns, "")?,
ns,
})
}
pub fn new_for_pid(pid: &str) -> io::Result<Self> {
let ns = format!("ad-{pid}");
Ok(Self {
inner: UnixClient::new_unix(&ns, "")?,
ns,
})
}
pub(crate) fn event_lines(&mut self, buffer: &str) -> io::Result<ReadLineIter<UnixStream>> {
self.inner.iter_lines(format!("buffers/{buffer}/event"))
}
pub(crate) fn write_event(&mut self, buffer: &str, event_line: &str) -> io::Result<()> {
self.inner
.write_str(format!("buffers/{buffer}/event"), 0, event_line)?;
Ok(())
}
pub fn log_events(&mut self) -> io::Result<impl Iterator<Item = io::Result<LogEvent>> + use<>> {
Ok(self
.inner
.iter_lines("log")?
.map(|line| LogEvent::from_str(&line)))
}
pub fn current_buffer(&mut self) -> io::Result<String> {
self.inner.read_str("buffers/current")
}
fn _read_buffer_file(&mut self, buffer: &str, file: &str) -> io::Result<String> {
self.inner.read_str(format!("buffers/{buffer}/{file}"))
}
pub fn read_dot(&mut self, buffer: &str) -> io::Result<String> {
self._read_buffer_file(buffer, "dot")
}
pub fn read_body(&mut self, buffer: &str) -> io::Result<String> {
self._read_buffer_file(buffer, "body")
}
pub fn read_addr(&mut self, buffer: &str) -> io::Result<String> {
self._read_buffer_file(buffer, "addr")
}
pub fn read_filename(&mut self, buffer: &str) -> io::Result<String> {
self._read_buffer_file(buffer, "filename")
}
pub fn read_xaddr(&mut self, buffer: &str) -> io::Result<String> {
self._read_buffer_file(buffer, "xaddr")
}
pub fn read_xdot(&mut self, buffer: &str) -> io::Result<String> {
self._read_buffer_file(buffer, "xdot")
}
fn _write_buffer_file(
&mut self,
buffer: &str,
file: &str,
offset: u64,
content: &[u8],
) -> io::Result<usize> {
self.inner
.write(format!("buffers/{buffer}/{file}"), offset, content)
}
pub fn write_dot(&mut self, buffer: &str, content: &str) -> io::Result<usize> {
self._write_buffer_file(buffer, "dot", 0, content.as_bytes())
}
pub fn append_to_body(&mut self, buffer: &str, content: &str) -> io::Result<usize> {
self._write_buffer_file(buffer, "body", 0, content.as_bytes())
}
pub fn write_addr(&mut self, buffer: &str, addr: &str) -> io::Result<usize> {
self._write_buffer_file(buffer, "addr", 0, addr.as_bytes())
}
pub fn write_xdot(&mut self, buffer: &str, content: &str) -> io::Result<usize> {
self._write_buffer_file(buffer, "xdot", 0, content.as_bytes())
}
pub fn write_xaddr(&mut self, buffer: &str, content: &str) -> io::Result<usize> {
self._write_buffer_file(buffer, "xaddr", 0, content.as_bytes())
}
pub fn ctl(&mut self, command: &str, args: &str) -> io::Result<()> {
self.inner
.write("ctl", 0, format!("{command} {args}").as_bytes())?;
Ok(())
}
pub fn echo(&mut self, msg: impl AsRef<str>) -> io::Result<()> {
self.ctl("echo", msg.as_ref())
}
pub fn open(&mut self, path: impl AsRef<str>) -> io::Result<()> {
self.ctl("open", path.as_ref())
}
pub fn open_in_new_window(&mut self, path: impl AsRef<str>) -> io::Result<()> {
self.ctl("open-in-new-window", path.as_ref())
}
pub fn reload_current_buffer(&mut self) -> io::Result<()> {
self.ctl("reload", "")
}
pub fn run_event_filter<F>(&mut self, buffer: &str, filter: F) -> io::Result<()>
where
F: EventFilter,
{
event::run_filter(buffer, filter, self)
}
pub fn body_writer(&self, bufid: &str) -> io::Result<BodyWriter> {
Ok(BodyWriter {
path: format!("buffers/{bufid}/body"),
client: UnixClient::new_unix(&self.ns, "")?,
})
}
}
#[derive(Debug)]
pub struct BodyWriter {
path: String,
client: UnixClient,
}
impl BodyWriter {
pub fn mark_clean(&mut self) -> io::Result<()> {
self.client.write("ctl", 0, "mark-clean".as_bytes())?;
Ok(())
}
}
impl Write for BodyWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.client.write(&self.path, 0, buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub enum LogEvent {
Open(usize),
Close(usize),
Focus(usize),
Save(usize),
}
impl FromStr for LogEvent {
type Err = io::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.contains('\n') {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"expected single line",
));
}
let (str_id, action) = s.split_once(' ').ok_or(io::Error::new(
io::ErrorKind::InvalidData,
"malformed log line: {s:?}",
))?;
let id: usize = str_id.parse().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"expected integer ID, got {str_id:?}",
)
})?;
let evt = match action {
"open" => Self::Open(id),
"close" => Self::Close(id),
"focus" => Self::Focus(id),
"save" => Self::Save(id),
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"unknown log action {action:?}",
));
}
};
Ok(evt)
}
}
fn open_9p_sockets() -> io::Result<Vec<String>> {
let mut ad_sockets = Vec::new();
for entry in fs::read_dir(socket_dir())? {
let entry = entry?;
let fname = entry.file_name();
if let Some(s) = fname.to_str()
&& s.starts_with("ad-")
{
ad_sockets.push(s.to_string());
}
}
Ok(ad_sockets)
}
#[derive(Debug)]
pub struct SessionMeta {
pub socket_name: String,
pub is_unresponsive: bool,
pub active_buffer_id: String,
pub buffers: Vec<BufferMeta>,
}
impl SessionMeta {
pub fn client_for_session(&self) -> io::Result<Client> {
Ok(Client {
inner: UnixClient::new_unix(&self.socket_name, "")?,
ns: self.socket_name.clone(),
})
}
pub fn remove_socket(&self) -> io::Result<()> {
fs::remove_file(socket_dir().join(&self.socket_name))
}
}
#[derive(Debug)]
pub struct BufferMeta {
pub id: String,
pub filename: String,
}
pub fn remove_unresponsive_sessions() -> io::Result<()> {
for session in list_open_sessions()?.into_iter() {
if session.is_unresponsive {
session.remove_socket()?;
}
}
Ok(())
}
pub fn list_open_sessions() -> io::Result<Vec<SessionMeta>> {
let mut sessions = Vec::new();
for ns in open_9p_sockets()?.into_iter() {
let mut client = match UnixClient::new_unix(&ns, "") {
Ok(client) => client,
Err(_) => {
sessions.push(SessionMeta {
socket_name: ns,
is_unresponsive: true,
active_buffer_id: String::new(),
buffers: Vec::new(),
});
continue;
}
};
let active_buffer_id = client.read_str("buffers/current")?;
let buffers = client
.read_str("buffers/index")?
.lines()
.map(|line| {
let mut it = line.split_whitespace();
let id = it.next().map(String::from).unwrap_or_default();
let filename = it.next().map(String::from).unwrap_or_default();
BufferMeta { id, filename }
})
.collect();
sessions.push(SessionMeta {
socket_name: ns,
is_unresponsive: false,
active_buffer_id,
buffers,
});
}
Ok(sessions)
}