ad_client/
lib.rs

1//! A simple 9p based client for interacting with ad
2#![warn(
3    clippy::complexity,
4    clippy::correctness,
5    clippy::style,
6    future_incompatible,
7    missing_debug_implementations,
8    missing_docs,
9    rust_2018_idioms,
10    rustdoc::all,
11    clippy::undocumented_unsafe_blocks
12)]
13use ninep::{
14    sansio::server::socket_dir,
15    sync::client::{ReadLineIter, UnixClient},
16};
17use std::{env, fs, io, io::Write, os::unix::net::UnixStream, str::FromStr};
18
19mod event;
20
21pub use ad_event::Source;
22pub use event::{EventFilter, Outcome};
23
24/// A simple 9p client for ad
25#[derive(Debug, Clone)]
26pub struct Client {
27    inner: UnixClient,
28    ns: String,
29}
30
31impl Client {
32    /// Create a new client connected to `ad` over it's 9p unix socket
33    pub fn new() -> io::Result<Self> {
34        let ns = match env::var("AD_PID") {
35            Ok(pid) => format!("ad-{pid}"),
36            Err(_) => "ad".to_string(),
37        };
38
39        Ok(Self {
40            inner: UnixClient::new_unix(&ns, "")?,
41            ns,
42        })
43    }
44
45    /// Create a new client connected to the `ad` session with the given pid
46    /// over it's 9p unix socket.
47    ///
48    /// When running under ad, the [Client::new] method will automatically find
49    /// and connect to it's parent session.
50    pub fn new_for_pid(pid: &str) -> io::Result<Self> {
51        let ns = format!("ad-{pid}");
52
53        Ok(Self {
54            inner: UnixClient::new_unix(&ns, "")?,
55            ns,
56        })
57    }
58
59    pub(crate) fn event_lines(&mut self, buffer: &str) -> io::Result<ReadLineIter<UnixStream>> {
60        self.inner.iter_lines(format!("buffers/{buffer}/event"))
61    }
62
63    pub(crate) fn write_event(&mut self, buffer: &str, event_line: &str) -> io::Result<()> {
64        self.inner
65            .write_str(format!("buffers/{buffer}/event"), 0, event_line)?;
66        Ok(())
67    }
68
69    /// Iterate over the log events emitted by ad
70    pub fn log_events(&mut self) -> io::Result<impl Iterator<Item = io::Result<LogEvent>> + use<>> {
71        Ok(self
72            .inner
73            .iter_lines("log")?
74            .map(|line| LogEvent::from_str(&line)))
75    }
76
77    /// Get the currently active buffer id.
78    pub fn current_buffer(&mut self) -> io::Result<String> {
79        self.inner.read_str("buffers/current")
80    }
81
82    fn _read_buffer_file(&mut self, buffer: &str, file: &str) -> io::Result<String> {
83        self.inner.read_str(format!("buffers/{buffer}/{file}"))
84    }
85
86    /// Read the contents of the dot of the given buffer
87    pub fn read_dot(&mut self, buffer: &str) -> io::Result<String> {
88        self._read_buffer_file(buffer, "dot")
89    }
90
91    /// Read the body of the given buffer.
92    pub fn read_body(&mut self, buffer: &str) -> io::Result<String> {
93        self._read_buffer_file(buffer, "body")
94    }
95
96    /// Read the current dot address of the given buffer.
97    pub fn read_addr(&mut self, buffer: &str) -> io::Result<String> {
98        self._read_buffer_file(buffer, "addr")
99    }
100
101    /// Read the filename of the given buffer
102    pub fn read_filename(&mut self, buffer: &str) -> io::Result<String> {
103        self._read_buffer_file(buffer, "filename")
104    }
105
106    /// Read the x-address of the given buffer.
107    ///
108    /// This is only used by the filesystem interface of `ad` and will not affect the current
109    /// editor state.
110    pub fn read_xaddr(&mut self, buffer: &str) -> io::Result<String> {
111        self._read_buffer_file(buffer, "xaddr")
112    }
113
114    /// Read the x-dot of the given buffer.
115    ///
116    /// This is only used by the filesystem interface of `ad` and will not affect the current
117    /// editor state.
118    pub fn read_xdot(&mut self, buffer: &str) -> io::Result<String> {
119        self._read_buffer_file(buffer, "xdot")
120    }
121
122    fn _write_buffer_file(
123        &mut self,
124        buffer: &str,
125        file: &str,
126        offset: u64,
127        content: &[u8],
128    ) -> io::Result<usize> {
129        self.inner
130            .write(format!("buffers/{buffer}/{file}"), offset, content)
131    }
132
133    /// Replace the dot of the given buffer with the provided string.
134    pub fn write_dot(&mut self, buffer: &str, content: &str) -> io::Result<usize> {
135        self._write_buffer_file(buffer, "dot", 0, content.as_bytes())
136    }
137
138    /// Append the provided string to the given buffer.
139    pub fn append_to_body(&mut self, buffer: &str, content: &str) -> io::Result<usize> {
140        self._write_buffer_file(buffer, "body", 0, content.as_bytes())
141    }
142
143    /// Set the addr of the given buffer.
144    pub fn write_addr(&mut self, buffer: &str, addr: &str) -> io::Result<usize> {
145        self._write_buffer_file(buffer, "addr", 0, addr.as_bytes())
146    }
147
148    /// Replace the xdot of the given buffer with the provided string.
149    pub fn write_xdot(&mut self, buffer: &str, content: &str) -> io::Result<usize> {
150        self._write_buffer_file(buffer, "xdot", 0, content.as_bytes())
151    }
152
153    /// Set the xaddr of the given buffer.
154    pub fn write_xaddr(&mut self, buffer: &str, content: &str) -> io::Result<usize> {
155        self._write_buffer_file(buffer, "xaddr", 0, content.as_bytes())
156    }
157
158    /// Send a control message to ad.
159    pub fn ctl(&mut self, command: &str, args: &str) -> io::Result<()> {
160        self.inner
161            .write("ctl", 0, format!("{command} {args}").as_bytes())?;
162
163        Ok(())
164    }
165
166    /// Echo a string message in the status line.
167    pub fn echo(&mut self, msg: impl AsRef<str>) -> io::Result<()> {
168        self.ctl("echo", msg.as_ref())
169    }
170
171    /// Open the requested file.
172    pub fn open(&mut self, path: impl AsRef<str>) -> io::Result<()> {
173        self.ctl("open", path.as_ref())
174    }
175
176    /// Open the requested file in a new window.
177    pub fn open_in_new_window(&mut self, path: impl AsRef<str>) -> io::Result<()> {
178        self.ctl("open-in-new-window", path.as_ref())
179    }
180
181    /// Reload the currently active buffer.
182    pub fn reload_current_buffer(&mut self) -> io::Result<()> {
183        self.ctl("reload", "")
184    }
185
186    /// Run a provided [EventFilter] until it exits or errors
187    pub fn run_event_filter<F>(&mut self, buffer: &str, filter: F) -> io::Result<()>
188    where
189        F: EventFilter,
190    {
191        event::run_filter(buffer, filter, self)
192    }
193
194    /// Create a [Write] impl that can be used to continuously write to the given path
195    pub fn body_writer(&self, bufid: &str) -> io::Result<BodyWriter> {
196        Ok(BodyWriter {
197            path: format!("buffers/{bufid}/body"),
198            client: UnixClient::new_unix(&self.ns, "")?,
199        })
200    }
201}
202
203/// A writer for appending to the body of a buffer
204#[derive(Debug)]
205pub struct BodyWriter {
206    path: String,
207    client: UnixClient,
208}
209
210impl BodyWriter {
211    /// Mark the buffer as being clean
212    pub fn mark_clean(&mut self) -> io::Result<()> {
213        self.client.write("ctl", 0, "mark-clean".as_bytes())?;
214
215        Ok(())
216    }
217}
218
219impl Write for BodyWriter {
220    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
221        self.client.write(&self.path, 0, buf)
222    }
223
224    fn flush(&mut self) -> io::Result<()> {
225        Ok(())
226    }
227}
228
229/// A message sent by the main editor thread to notify the fs thread that
230/// the current buffer list has changed.
231#[derive(Debug, Clone, Copy)]
232pub enum LogEvent {
233    /// A newly created buffer
234    Open(usize),
235    /// A buffer that has now been closed and needs removing from state
236    Close(usize),
237    /// A change to the currently active buffer
238    Focus(usize),
239    /// A buffer was saved
240    Save(usize),
241}
242
243impl FromStr for LogEvent {
244    type Err = io::Error;
245
246    fn from_str(s: &str) -> Result<Self, Self::Err> {
247        let s = s.trim();
248        if s.contains('\n') {
249            return Err(io::Error::new(
250                io::ErrorKind::InvalidData,
251                "expected single line",
252            ));
253        }
254
255        let (str_id, action) = s.split_once(' ').ok_or(io::Error::new(
256            io::ErrorKind::InvalidData,
257            "malformed log line: {s:?}",
258        ))?;
259
260        let id: usize = str_id.parse().map_err(|_| {
261            io::Error::new(
262                io::ErrorKind::InvalidData,
263                "expected integer ID, got {str_id:?}",
264            )
265        })?;
266
267        let evt = match action {
268            "open" => Self::Open(id),
269            "close" => Self::Close(id),
270            "focus" => Self::Focus(id),
271            "save" => Self::Save(id),
272            _ => {
273                return Err(io::Error::new(
274                    io::ErrorKind::InvalidData,
275                    "unknown log action {action:?}",
276                ));
277            }
278        };
279
280        Ok(evt)
281    }
282}
283
284fn open_9p_sockets() -> io::Result<Vec<String>> {
285    let mut ad_sockets = Vec::new();
286    for entry in fs::read_dir(socket_dir())? {
287        let entry = entry?;
288        let fname = entry.file_name();
289        if let Some(s) = fname.to_str()
290            && s.starts_with("ad-")
291        {
292            ad_sockets.push(s.to_string());
293        }
294    }
295
296    Ok(ad_sockets)
297}
298
299/// Metadata for an ad editor session.
300#[derive(Debug)]
301pub struct SessionMeta {
302    /// The socket name within [socket_dir] for this session.
303    pub socket_name: String,
304    /// Whether or not the session is currently unresponsive.
305    ///
306    /// A session can become unresponsive when it crashes before successfully
307    /// removing it's filesystem socket.
308    pub is_unresponsive: bool,
309    /// The id of the currently active buffer.
310    pub active_buffer_id: String,
311    /// Metadata for the buffers open in this session.
312    pub buffers: Vec<BufferMeta>,
313}
314
315impl SessionMeta {
316    /// Create a new [Client] for this session.
317    pub fn client_for_session(&self) -> io::Result<Client> {
318        Ok(Client {
319            inner: UnixClient::new_unix(&self.socket_name, "")?,
320            ns: self.socket_name.clone(),
321        })
322    }
323
324    /// Remove this session's filesystem socket.
325    pub fn remove_socket(&self) -> io::Result<()> {
326        fs::remove_file(socket_dir().join(&self.socket_name))
327    }
328}
329
330/// Metadata for an open buffer within an ad editor session.
331#[derive(Debug)]
332pub struct BufferMeta {
333    /// The id of the buffer.
334    pub id: String,
335    /// The full filename of the buffer.
336    pub filename: String,
337}
338
339/// Call [SessionMeta::remove_socket] for all currently unresponsive editor sessions.
340pub fn remove_unresponsive_sessions() -> io::Result<()> {
341    for session in list_open_sessions()?.into_iter() {
342        if session.is_unresponsive {
343            session.remove_socket()?;
344        }
345    }
346
347    Ok(())
348}
349
350/// List open `ad` editor sessions and their current state.
351pub fn list_open_sessions() -> io::Result<Vec<SessionMeta>> {
352    let mut sessions = Vec::new();
353
354    for ns in open_9p_sockets()?.into_iter() {
355        let mut client = match UnixClient::new_unix(&ns, "") {
356            Ok(client) => client,
357            Err(_) => {
358                sessions.push(SessionMeta {
359                    socket_name: ns,
360                    is_unresponsive: true,
361                    active_buffer_id: String::new(),
362                    buffers: Vec::new(),
363                });
364                continue;
365            }
366        };
367        let active_buffer_id = client.read_str("buffers/current")?;
368        let buffers = client
369            .read_str("buffers/index")?
370            .lines()
371            .map(|line| {
372                let mut it = line.split_whitespace();
373                let id = it.next().map(String::from).unwrap_or_default();
374                let filename = it.next().map(String::from).unwrap_or_default();
375
376                BufferMeta { id, filename }
377            })
378            .collect();
379
380        sessions.push(SessionMeta {
381            socket_name: ns,
382            is_unresponsive: false,
383            active_buffer_id,
384            buffers,
385        });
386    }
387
388    Ok(sessions)
389}