diff --git a/Cargo.lock b/Cargo.lock index 2f0b90e..f623c33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,20 @@ dependencies = [ name = "ad_event" version = "0.1.1" dependencies = [ + "serde", + "serde_json", "simple_test_case", ] +[[package]] +name = "ad_repl" +version = "0.1.0" +dependencies = [ + "ad_client", + "anyhow", + "subprocess", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -52,6 +63,12 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +[[package]] +name = "anyhow" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" + [[package]] name = "autocfg" version = "1.4.0" @@ -445,18 +462,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", @@ -465,9 +482,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -501,11 +518,21 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "subprocess" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "syn" -version = "2.0.79" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", diff --git a/crates/ad_client/examples/event_filter.rs b/crates/ad_client/examples/event_filter.rs index 084c627..8e94d48 100644 --- a/crates/ad_client/examples/event_filter.rs +++ b/crates/ad_client/examples/event_filter.rs @@ -1,7 +1,7 @@ -use ad_client::{Client, EventFilter, Outcome}; -use std::error::Error; +use ad_client::{Client, EventFilter, Outcome, Source}; +use std::io; -fn main() -> Result<(), Box> { +fn main() -> io::Result<()> { let mut client = Client::new()?; client.open(".")?; let buffer = client.current_buffer()?; @@ -15,11 +15,12 @@ struct Filter; impl EventFilter for Filter { fn handle_load( &mut self, + _src: Source, from: usize, to: usize, txt: &str, _client: &mut Client, - ) -> Result> { + ) -> io::Result { println!("got load: {from}->{to} {txt:?}"); match txt { "README.md" => Ok(Outcome::Passthrough), diff --git a/crates/ad_client/src/event.rs b/crates/ad_client/src/event.rs index 9ba36d8..ff4d15e 100644 --- a/crates/ad_client/src/event.rs +++ b/crates/ad_client/src/event.rs @@ -1,7 +1,7 @@ //! Handling of event filtering use crate::Client; -use ad_event::{FsysEvent, Kind}; -use std::error::Error; +use ad_event::{FsysEvent, Kind, Source}; +use std::io; /// Outcome of handling an event within an [EventFilter] #[derive(Debug)] @@ -16,9 +16,6 @@ pub enum Outcome { Exit, } -// FIXME: once the tagging of events in the editor is working correctly this should have methods -// that are specificy to the source (Mouse / Keyboard / Fsys) rather than just the kind. - /// An event filter takes control over a buffer's events file and handles processing the events /// that come through. Any events without a corresponding handler are written back to ad for /// internal processing. @@ -27,86 +24,81 @@ pub trait EventFilter { /// Handle text being inserted into the buffer body fn handle_insert( &mut self, + src: Source, from: usize, to: usize, txt: &str, client: &mut Client, - ) -> Result> { - Ok(Outcome::Passthrough) + ) -> io::Result { + Ok(Outcome::Handled) } /// Handle text being deleted from the buffer body fn handle_delete( &mut self, + src: Source, from: usize, to: usize, client: &mut Client, - ) -> Result> { - Ok(Outcome::Passthrough) + ) -> io::Result { + Ok(Outcome::Handled) } /// Handle a load event in the body fn handle_load( &mut self, + src: Source, from: usize, to: usize, txt: &str, client: &mut Client, - ) -> Result> { + ) -> io::Result { Ok(Outcome::Passthrough) } /// Handle an execute event in the body fn handle_execute( &mut self, + src: Source, from: usize, to: usize, txt: &str, client: &mut Client, - ) -> Result> { + ) -> io::Result { Ok(Outcome::Passthrough) } } -pub(crate) fn run_filter( - buffer: &str, - mut filter: F, - client: &mut Client, -) -> Result<(), Box> +pub(crate) fn run_filter(buffer: &str, mut filter: F, client: &mut Client) -> io::Result<()> where F: EventFilter, { - let mut buf = String::new(); for line in client.event_lines(buffer)? { - buf.push_str(&line); - let evts = match FsysEvent::try_from_str(&buf) { - Ok(evts) => evts, - Err(_) => continue, - }; - buf.clear(); + let evt = FsysEvent::try_from_str(&line) + .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; - for evt in evts.into_iter() { - let outcome = match evt.kind { - Kind::LoadBody => filter.handle_load(evt.ch_from, evt.ch_to, &evt.txt, client)?, - Kind::ExecuteBody => { - filter.handle_execute(evt.ch_from, evt.ch_to, &evt.txt, client)? - } - Kind::InsertBody => { - filter.handle_insert(evt.ch_from, evt.ch_to, &evt.txt, client)? - } - Kind::DeleteBody => filter.handle_delete(evt.ch_from, evt.ch_to, client)?, - _ => Outcome::Passthrough, - }; + let outcome = match evt.kind { + Kind::LoadBody => { + filter.handle_load(evt.source, evt.ch_from, evt.ch_to, &evt.txt, client)? + } + Kind::ExecuteBody => { + filter.handle_execute(evt.source, evt.ch_from, evt.ch_to, &evt.txt, client)? + } + Kind::InsertBody => { + filter.handle_insert(evt.source, evt.ch_from, evt.ch_to, &evt.txt, client)? + } + Kind::DeleteBody => filter.handle_delete(evt.source, evt.ch_from, evt.ch_to, client)?, + _ => Outcome::Passthrough, + }; - match outcome { - Outcome::Handled => (), - Outcome::Passthrough => client.write_event(buffer, &evt.as_event_file_line())?, - Outcome::PassthroughAndExit => { - client.write_event(buffer, &evt.as_event_file_line())?; - return Ok(()); - } - Outcome::Exit => return Ok(()), + match outcome { + Outcome::Handled => (), + Outcome::Passthrough => client.write_event(buffer, &evt.as_event_file_line())?, + Outcome::PassthroughAndExit => { + client.write_event(buffer, &evt.as_event_file_line())?; + return Ok(()); } + Outcome::Exit => return Ok(()), } } diff --git a/crates/ad_client/src/lib.rs b/crates/ad_client/src/lib.rs index 6bb0a5c..6f36dbe 100644 --- a/crates/ad_client/src/lib.rs +++ b/crates/ad_client/src/lib.rs @@ -11,14 +11,15 @@ clippy::undocumented_unsafe_blocks )] use ninep::client::{ReadLineIter, UnixClient}; -use std::{error::Error, io, os::unix::net::UnixStream}; +use std::{io, io::Write, os::unix::net::UnixStream}; mod event; +pub use ad_event::Source; pub use event::{EventFilter, Outcome}; /// A simple 9p client for ad -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Client { inner: UnixClient, } @@ -102,9 +103,9 @@ impl Client { self._write_buffer_file(buffer, "dot", 0, content.as_bytes()) } - /// Write the provided string to the specified offset in the given buffer. - pub fn write_body(&mut self, buffer: &str, offset: u64, content: &str) -> io::Result { - self._write_buffer_file(buffer, "body", offset, content.as_bytes()) + /// Append the provided string to the given buffer. + pub fn append_to_body(&mut self, buffer: &str, content: &str) -> io::Result { + self._write_buffer_file(buffer, "body", 0, content.as_bytes()) } /// Set the addr of the given buffer. @@ -113,8 +114,8 @@ impl Client { } /// Replace the xdot of the given buffer with the provided string. - pub fn write_xdot(&mut self, buffer: &str, offset: u64, content: &str) -> io::Result { - self._write_buffer_file(buffer, "xdot", offset, content.as_bytes()) + pub fn write_xdot(&mut self, buffer: &str, content: &str) -> io::Result { + self._write_buffer_file(buffer, "xdot", 0, content.as_bytes()) } /// Set the xaddr of the given buffer. @@ -122,7 +123,8 @@ impl Client { self._write_buffer_file(buffer, "xaddr", 0, content.as_bytes()) } - fn _ctl(&mut self, command: &str, args: &str) -> io::Result<()> { + /// Send a control message to ad. + pub fn ctl(&mut self, command: &str, args: &str) -> io::Result<()> { self.inner .write("ctl", 0, format!("{command} {args}").as_bytes())?; @@ -131,24 +133,56 @@ impl Client { /// Echo a string message in the status line. pub fn echo(&mut self, msg: impl AsRef) -> io::Result<()> { - self._ctl("echo", msg.as_ref()) + self.ctl("echo", msg.as_ref()) } /// Open the requested file. pub fn open(&mut self, path: impl AsRef) -> io::Result<()> { - self._ctl("open", path.as_ref()) + self.ctl("open", path.as_ref()) + } + + /// Open the requested file in a new window. + pub fn open_in_new_window(&mut self, path: impl AsRef) -> io::Result<()> { + self.ctl("open-in-new-window", path.as_ref()) } /// Reload the currently active buffer. pub fn reload_current_buffer(&mut self) -> io::Result<()> { - self._ctl("reload", "") + self.ctl("reload", "") } /// Run a provided [EventFilter] until it exits or errors - pub fn run_event_filter(&mut self, buffer: &str, filter: F) -> Result<(), Box> + pub fn run_event_filter(&mut self, buffer: &str, filter: F) -> io::Result<()> where F: EventFilter, { event::run_filter(buffer, filter, self) } + + /// Create a [Write] impl that can be used to continuously write to the given path + pub fn body_writer(&self, bufid: &str) -> io::Result { + let client = UnixClient::new_unix("ad", "")?; + + Ok(BodyWriter { + path: format!("buffers/{bufid}/body"), + client, + }) + } +} + +/// A writer for appending to the body of a buffer +#[derive(Debug)] +pub struct BodyWriter { + path: String, + client: UnixClient, +} + +impl Write for BodyWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.client.write(&self.path, 0, buf) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } } diff --git a/crates/ad_event/Cargo.toml b/crates/ad_event/Cargo.toml index fbef11a..d2e75d1 100644 --- a/crates/ad_event/Cargo.toml +++ b/crates/ad_event/Cargo.toml @@ -17,6 +17,8 @@ keywords = [ "terminal", "editor", "text-editor", ] categories = [ "development-tools", "text-editors", "command-line-utilities" ] [dependencies] +serde = { version = "1.0.214", features = ["derive"] } +serde_json = "1.0.132" [dev-dependencies] simple_test_case = "1" diff --git a/crates/ad_event/src/lib.rs b/crates/ad_event/src/lib.rs index 061282d..5beaefa 100644 --- a/crates/ad_event/src/lib.rs +++ b/crates/ad_event/src/lib.rs @@ -1,83 +1,52 @@ //! A shared event message format between ad and clients +use serde::{Deserialize, Serialize}; + type Result = std::result::Result; -const EOF: &str = "unexpected EOF"; const MAX_CHARS: usize = 256; -macro_rules! impl_charconv { - ($name:ident, $($variant:ident <=> $ch:expr,)+) => { - impl $name { - fn to_char(self) -> char { - match self { - $($name::$variant => $ch,)+ - } - } - - fn try_from_iter(it: &mut I) -> Result - where - I: Iterator, - { - let ch = it.next().ok_or_else(|| EOF.to_string())?; - match ch { - $($ch => Ok($name::$variant),)+ - ch => Err(format!("unknown {} variant: {}", stringify!($name), ch)), - } - } - - } - } -} - /// acme makes a distinction between direct writes to /body and /tag vs /// text entering the buffer via one of the other fsys files but I'm not /// sure if I need that initially? As and when it looks useful I can add it. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Source { + #[serde(rename = "K")] Keyboard, + #[serde(rename = "M")] Mouse, + #[serde(rename = "F")] Fsys, } -impl_charconv! { - Source, - Keyboard <=> 'K', - Mouse <=> 'M', - Fsys <=> 'F', -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Kind { + #[serde(rename = "I")] InsertBody, + #[serde(rename = "D")] DeleteBody, + #[serde(rename = "X")] ExecuteBody, + #[serde(rename = "L")] LoadBody, + #[serde(rename = "i")] InsertTag, + #[serde(rename = "d")] DeleteTag, + #[serde(rename = "x")] ExecuteTag, + #[serde(rename = "l")] LoadTag, + #[serde(rename = "A")] ChordedArgument, } -impl_charconv! { - Kind, - InsertBody <=> 'I', - DeleteBody <=> 'D', - ExecuteBody <=> 'X', - LoadBody <=> 'L', - InsertTag <=> 'i', - DeleteTag <=> 'd', - ExecuteTag <=> 'x', - LoadTag <=> 'l', - ChordedArgument <=> 'A', -} - -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct FsysEvent { pub source: Source, pub kind: Kind, pub ch_from: usize, pub ch_to: usize, - pub n_chars: usize, + pub truncated: bool, pub txt: String, } @@ -86,109 +55,36 @@ impl FsysEvent { /// /// The `txt` field of events is limited to [MAX_CHARS] or up until the first newline character /// and will be truncated if larger. Delete events are always truncated to zero length. - pub fn new(source: Source, kind: Kind, ch_from: usize, ch_to: usize, txt: &str) -> Self { + pub fn new(source: Source, kind: Kind, ch_from: usize, ch_to: usize, raw_txt: &str) -> Self { let txt = match kind { Kind::DeleteTag | Kind::DeleteBody => String::new(), - _ => txt - .lines() - .next() - .unwrap_or_default() - .chars() - .take(MAX_CHARS) - .collect(), + _ => raw_txt.chars().take(MAX_CHARS).collect(), }; - let n_chars = txt.chars().count(); + let truncated = txt != raw_txt; Self { source, kind, ch_from, ch_to, - n_chars, + truncated, txt, } } pub fn as_event_file_line(&self) -> String { - let (txt, n_chars) = match self.kind { - Kind::DeleteTag | Kind::DeleteBody => ("", 0), - _ => (self.txt.as_str(), self.n_chars), - }; - - format!( - "{} {} {} {} {} | {}\n", - self.source.to_char(), - self.kind.to_char(), - self.ch_from, - self.ch_to, - n_chars, - txt - ) - } - - pub fn try_from_str(s: &str) -> Result> { - let mut it = s.chars().peekable(); - let mut events = Vec::new(); - loop { - if it.peek().is_none() { - return Ok(events); - } - let evt = Self::try_single_from_iter(&mut it)?; - events.push(evt); - } + format!("{}\n", serde_json::to_string(self).unwrap()) } - pub fn try_single_from_iter(it: &mut I) -> Result - where - I: Iterator, - { - let source = Source::try_from_iter(it)?; - it.next(); // consume the space - let kind = Kind::try_from_iter(it)?; - it.next(); // consume the space - let ch_from = read_usize(it)?; - let ch_to = read_usize(it)?; - let n_chars = read_usize(it)?; - - if n_chars > MAX_CHARS { + pub fn try_from_str(s: &str) -> Result { + let evt: Self = + serde_json::from_str(s.trim()).map_err(|e| format!("invalid event: {e}"))?; + if evt.txt.chars().count() > MAX_CHARS { return Err(format!("txt field too long: max chars = {MAX_CHARS}")); } - // skip the '| ' delimiter before the text body starts - let txt: Vec = it.skip(2).take(n_chars).collect(); - it.next(); // consume the trailing newline - - if txt.len() != n_chars { - Err(format!( - "expected {n_chars} chars in txt but got {}", - txt.len() - )) - } else { - Ok(Self { - source, - kind, - ch_from, - ch_to, - n_chars, - txt: txt.into_iter().collect(), - }) - } - } -} - -fn read_usize(it: &mut I) -> Result -where - I: Iterator, -{ - let mut buf = String::new(); - for ch in it { - if ch == ' ' { - break; - } - buf.push(ch); + Ok(evt) } - - buf.parse::().map_err(|e| e.to_string()) } #[cfg(test)] @@ -204,14 +100,26 @@ mod tests { #[test] fn as_event_file_line_works() { let line = evt("a").as_event_file_line(); - assert_eq!(line, "K I 17 18 1 | a\n"); + assert_eq!( + line, + "{\"source\":\"K\",\"kind\":\"I\",\"ch_from\":17,\"ch_to\":18,\"truncated\":false,\"txt\":\"a\"}\n" + ); + } + + #[test] + fn as_event_file_line_works_for_newline() { + let line = evt("\n").as_event_file_line(); + assert_eq!( + line, + "{\"source\":\"K\",\"kind\":\"I\",\"ch_from\":17,\"ch_to\":18,\"truncated\":false,\"txt\":\"\\n\"}\n" + ); } #[test] fn txt_length_is_truncated_in_new() { let long_txt = "a".repeat(MAX_CHARS + 10); let e = FsysEvent::new(Source::Keyboard, Kind::InsertBody, 17, 283, &long_txt); - assert_eq!(e.n_chars, MAX_CHARS); + assert!(e.truncated); } #[test_case(Kind::DeleteBody; "delete in body")] @@ -219,14 +127,15 @@ mod tests { #[test] fn txt_is_removed_for_delete_events_if_provided(kind: Kind) { let e = FsysEvent::new(Source::Keyboard, kind, 42, 42 + 17, "some deleted text"); - assert_eq!(e.n_chars, 0); + assert!(e.truncated); + assert!(e.txt.is_empty()); } #[test] fn txt_length_is_checked_on_parse() { let long_txt = "a".repeat(MAX_CHARS + 10); let line = format!("K I 17 283 266 | {long_txt}"); - let res = FsysEvent::try_single_from_iter(&mut line.chars()); + let res = FsysEvent::try_from_str(&line); assert!(res.is_err(), "expected error, got {res:?}"); } @@ -240,24 +149,8 @@ mod tests { fn round_trip_single_works(s: &str) { let e = evt(s); let line = e.as_event_file_line(); - let parsed = FsysEvent::try_single_from_iter(&mut line.chars()).expect("to parse"); + let parsed = FsysEvent::try_from_str(&line).expect("to parse"); assert_eq!(parsed, e); } - - #[test] - fn round_trip_multi_works() { - let events = vec![ - evt("a"), - evt("testing"), - evt("testing testing 1 2 3"), - evt("Hello, 世界"), - evt("testing testing\n1 2 3"), - evt("testing testing 1 2 3\n"), - ]; - let s: String = events.iter().map(|e| e.as_event_file_line()).collect(); - let parsed = FsysEvent::try_from_str(&s).expect("to parse"); - - assert_eq!(parsed, events); - } } diff --git a/crates/ad_repl/Cargo.toml b/crates/ad_repl/Cargo.toml new file mode 100644 index 0000000..3605c62 --- /dev/null +++ b/crates/ad_repl/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ad_repl" +version = "0.1.0" +edition = "2021" + +[dependencies] +ad_client = { version = "0.1.0", path = "../ad_client" } +anyhow = "1.0.93" +subprocess = "0.2.9" diff --git a/crates/ad_repl/README.md b/crates/ad_repl/README.md new file mode 100644 index 0000000..61c68b0 --- /dev/null +++ b/crates/ad_repl/README.md @@ -0,0 +1,9 @@ +# ad_repl + +A simple REPL that syncs a shell session with an ad buffer. + + - "Execute" is defined to be "send as input to the shell". + - Hitting return at the end of the buffer will send that line to the shell. + - Running "clear" will clear the ad buffer + - Running "exit" will close the shell subprocess as well as the ad buffer + diff --git a/crates/ad_repl/src/main.rs b/crates/ad_repl/src/main.rs new file mode 100644 index 0000000..6d663bd --- /dev/null +++ b/crates/ad_repl/src/main.rs @@ -0,0 +1,137 @@ +//! A simple REPL that syncs a shell session with an ad buffer. +//! +//! - "Execute" is defined to be "send as input to the shell". +//! - Hitting return at the end of the buffer will send that line to the shell. +//! - Running "clear" will clear the ad buffer +//! - Running "exit" will close the shell subprocess as well as the ad buffer +use ad_client::{Client, EventFilter, Outcome, Source}; +use std::{ + fs::File, + io::{self, copy, Write}, + process::exit, + thread::spawn, +}; +use subprocess::{Popen, PopenConfig, Redirection}; + +fn main() -> io::Result<()> { + let mut client = Client::new()?; + client.open_in_new_window("+win")?; + let buffer_id = client.current_buffer()?; + + let mut proc = Popen::create( + &["sh", "-i"], + PopenConfig { + stdin: Redirection::Pipe, + stdout: Redirection::Pipe, + stderr: Redirection::Merge, + ..Default::default() + }, + ) + .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + let stdin = proc.stdin.take().unwrap(); + let mut stdout = proc.stdout.take().unwrap(); + let mut w = client.body_writer(&buffer_id)?; + + spawn(move || { + _ = copy(&mut stdout, &mut w); + }); + + let f = Filter { + proc, + buffer_id: buffer_id.clone(), + stdin, + }; + + client.run_event_filter(&buffer_id, f).unwrap(); + + Ok(()) +} + +struct Filter { + proc: Popen, + buffer_id: String, + stdin: File, +} + +impl Drop for Filter { + fn drop(&mut self) { + _ = self.proc.kill(); + } +} + +impl Filter { + fn send_input(&mut self, input: &str, client: &mut Client) -> io::Result { + match input.trim() { + "clear" => { + client.write_xaddr(&self.buffer_id, ",")?; + client.write_xdot(&self.buffer_id, "$ ")?; + client.write_addr(&self.buffer_id, "$")?; + return Ok(Outcome::Handled); + } + + "exit" => { + client.ctl("db!", "")?; + self.proc.kill()?; + exit(0); + } + + _ => (), + } + + self.stdin.write_all(input.as_bytes())?; + if !input.ends_with("\n") { + self.stdin.write_all(b"\n")?; + } + + Ok(Outcome::Handled) + } +} + +impl EventFilter for Filter { + fn handle_insert( + &mut self, + src: Source, + _from: usize, + _to: usize, + txt: &str, + client: &mut Client, + ) -> io::Result { + if src == Source::Fsys { + // This is us writing to the body so move dot to EOF + client.write_addr(&self.buffer_id, "$")?; + return Ok(Outcome::Handled); + } + + if txt == "\n" { + client.write_xaddr(&self.buffer_id, "$")?; + let xaddr = client.read_xaddr(&self.buffer_id)?; + let addr = client.read_addr(&self.buffer_id)?; + + if xaddr == addr { + client.write_xaddr(&self.buffer_id, "$-1")?; + let raw = client.read_xdot(&self.buffer_id)?; + let s = match raw.strip_prefix("$ ") { + Some(s) => s, + None => &raw, + }; + return self.send_input(s, client); + } + } + + Ok(Outcome::Handled) + } + + fn handle_execute( + &mut self, + _src: Source, + _from: usize, + _to: usize, + txt: &str, + client: &mut Client, + ) -> io::Result { + client.append_to_body(&self.buffer_id, "\n")?; + let outcome = self.send_input(txt, client)?; + + Ok(outcome) + } +} diff --git a/crates/ninep/src/client.rs b/crates/ninep/src/client.rs index f509eda..78c7eae 100644 --- a/crates/ninep/src/client.rs +++ b/crates/ninep/src/client.rs @@ -417,7 +417,7 @@ where let chunk_size = (self.msize - header_size) as usize; let mut inner = self.inner(); - while cur <= len { + while cur < len { let end = min(cur + chunk_size, len); let resp = inner.send( 0, @@ -546,8 +546,10 @@ where loop { match self.buf.iter().position(|&b| b == b'\n') { Some(pos) => { - let (line, remaining) = self.buf.split_at(pos + 1); - let s = String::from_utf8(line.to_vec()).ok(); + let (raw_line, remaining) = self.buf.split_at(pos + 1); + let mut line = raw_line.to_vec(); + line.pop(); + let s = String::from_utf8(line).ok(); self.buf = remaining.to_vec(); return s; } @@ -560,6 +562,9 @@ where if data.is_empty() { self.at_eof = true; + if self.buf.is_empty() { + return None; + } return String::from_utf8(mem::take(&mut self.buf)).ok(); } diff --git a/src/fsys/event.rs b/src/fsys/event.rs index 9ecc491..964433b 100644 --- a/src/fsys/event.rs +++ b/src/fsys/event.rs @@ -109,15 +109,14 @@ pub fn run_threaded_input_listener(event_rx: Receiver) -> Sender) -> Result { let n_bytes_written = s.len(); - for evt in FsysEvent::try_from_str(s)?.into_iter() { - let req = match evt.kind { - Kind::LoadBody | Kind::LoadTag => Req::LoadInBuffer { id, txt: evt.txt }, - Kind::ExecuteBody | Kind::ExecuteTag => Req::ExecuteInBuffer { id, txt: evt.txt }, - _ => continue, - }; - - Message::send(req, tx)?; - } + let evt = FsysEvent::try_from_str(s)?; + let req = match evt.kind { + Kind::LoadBody | Kind::LoadTag => Req::LoadInBuffer { id, txt: evt.txt }, + Kind::ExecuteBody | Kind::ExecuteTag => Req::ExecuteInBuffer { id, txt: evt.txt }, + _ => return Ok(n_bytes_written), + }; + + Message::send(req, tx)?; Ok(n_bytes_written) } diff --git a/src/ui/layout.rs b/src/ui/layout.rs index f3cb7fd..9c10f1a 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -75,8 +75,14 @@ impl Layout { pub(crate) fn open_or_focus>( &mut self, path: P, - new_window: bool, + mut new_window: bool, ) -> io::Result> { + if self.buffers.is_empty_scratch() { + // in the case where we only have an empty scratch buffer present, we always + // replace the current buffer with the one that is newly opened. + new_window = false; + } + let opt = self.buffers.open_or_focus(path)?; let id = self.active_buffer().id;