diff options
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | src/i3bar_protocol.rs | 99 |
2 files changed, 78 insertions, 24 deletions
@@ -10,14 +10,13 @@ It works on my machineā¢, but this program is in early stage of development. Full compatibility is desired, but right now not everything is implemented. -I've tested [`i3status-rs`](https://github.com/greshake/i3status-rust) and [`bumblebee-status`](https://github.com/tobi-wan-kenobi/bumblebee-status) and everything seems usable. +I've tested [`i3status-rs`](https://github.com/greshake/i3status-rust), [`bumblebee-status`](https://github.com/tobi-wan-kenobi/bumblebee-status) and [`py3status`](https://github.com/ultrabug/py3status) and everything seems usable. A list of things that are missing (for now): - `short_text` - `border[_top|_right|_bottom|_left]` - Click events lack some info (IDK if anyone actually relies on `x`, `y`, `width`, etc.) - Multiple seat support (river doesn't support this either, so it's fine for now) -- The JSON parsing implementation is not "streaming": all blocks should be on the same line ## Installation diff --git a/src/i3bar_protocol.rs b/src/i3bar_protocol.rs index d838fb1..7da061c 100644 --- a/src/i3bar_protocol.rs +++ b/src/i3bar_protocol.rs @@ -3,6 +3,7 @@ use crate::pointer_btn::PointerBtn; use crate::text::Align; use serde::Deserialize; use serde::Serialize; +use serde_json::{Deserializer, Result as SerdeResult}; use std::io::{Error, ErrorKind, Result}; #[derive(Deserialize, Default)] @@ -81,11 +82,17 @@ pub enum Protocol { Unknown, PlainText(Option<String>), JsonNotStarted(JsonHeader), - Json(JsonHeader, Option<Vec<Block>>), + Json(JsonHeader, Option<Vec<Block>>, Option<String>), } impl Protocol { pub fn process_line(&mut self, line: String) -> Result<()> { + macro_rules! invalid { + ($fmt:expr) => { + return Err(Error::new(ErrorKind::InvalidData, format!($fmt))) + }; + } + match self { Self::Unknown => { if let Ok(header) = serde_json::from_str::<JsonHeader>(&line) { @@ -98,32 +105,38 @@ impl Protocol { } Self::PlainText(s) => *s = Some(line), Self::JsonNotStarted(header) => { - let line = line.trim(); + let line = line.trim_start(); if !line.is_empty() { - if !line.starts_with('[') && line.is_empty() { - return Err(Error::new(ErrorKind::InvalidData, "Expected '['")); + if !line.starts_with('[') { + invalid!("Expected '['"); } - *self = Self::Json(*header, None); + *self = Self::Json(*header, None, None); self.process_line(line[1..].to_owned())?; } } - Self::Json(_, blocks) => { - let line = line.trim(); - if !line.is_empty() { - if !line.ends_with(',') { - return Err(Error::new(ErrorKind::InvalidData, "Expected ','")); - } - *blocks = match serde_json::from_str::<Vec<Block>>(&line[..(line.len() - 1)]) { - Ok(b) => Some(b), - Err(e) => { - return Err(Error::new( - ErrorKind::InvalidData, - format!("Invalid JSON: {e}"), - )) + Self::Json(_, blocks, start_orig) => match start_orig { + Some(start) => { + start.push_str(&line); + match de_last::<Vec<Block>>(start) { + Err(e) => invalid!("Invalid JSON: {e}"), + Ok((rem, new_blocks)) => { + *start_orig = (!rem.is_empty()).then(|| rem.to_owned()); + if let Some(new_blocks) = new_blocks { + *blocks = Some(new_blocks); + } } - }; + } } - } + None => match de_last::<Vec<Block>>(&line) { + Err(e) => invalid!("Invalid JSON: {e}"), + Ok((rem, new_blocks)) => { + *start_orig = (!rem.is_empty()).then(|| rem.to_owned()); + if let Some(new_blocks) = new_blocks { + *blocks = Some(new_blocks); + } + } + }, + }, } Ok(()) } @@ -136,15 +149,57 @@ impl Protocol { ..Default::default() }]), Self::JsonNotStarted(_) => None, - Self::Json(_, blocks) => blocks.take(), + Self::Json(_, blocks, _) => blocks.take(), } } pub fn supports_clicks(&self) -> bool { match self { Self::JsonNotStarted(h) => h.click_events, - Self::Json(h, _) => h.click_events, + Self::Json(h, _, _) => h.click_events, _ => false, } } } + +/// Deserialize the last complete object. Returns (`remaining`, `object`). See tests for examples. +fn de_last<'a, T: Deserialize<'a>>(mut s: &'a str) -> SerdeResult<(&'a str, Option<T>)> { + let mut last = None; + s = s.trim_start_matches(|x: char| x.is_ascii_whitespace() || x == ','); + loop { + let (rem, obj) = de_once(s)?; + last = match obj { + Some(obj) => Some(obj), + None => return Ok((rem, last)), + }; + s = rem.trim_start_matches(|x: char| x.is_ascii_whitespace() || x == ','); + } +} + +/// Deserialize the first complete object. Returns (`remaining`, `object`). +fn de_once<'a, T: Deserialize<'a>>(s: &'a str) -> SerdeResult<(&'a str, Option<T>)> { + let mut de = Deserializer::from_str(s).into_iter(); + match de.next() { + Some(Ok(obj)) => Ok((&s[de.byte_offset()..], Some(obj))), + Some(Err(e)) if e.is_eof() => Ok((&s[de.byte_offset()..], None)), + Some(Err(e)) => Err(e), + None => Ok((&s[de.byte_offset()..], None)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn streaming_json() { + let s = ",[2], [3], [4, 3],[32][3] "; + assert_eq!(de_last::<Vec<u8>>(s).unwrap(), ("", Some(vec![3]))); + + let s = ",[2], [3], [4, 3],[32][3] [2, 4"; + assert_eq!(de_last::<Vec<u8>>(s).unwrap(), ("[2, 4", Some(vec![3]))); + + let s = ",[2], [3], [4, 3],[32] invalid"; + assert!(de_last::<Vec<u8>>(s).is_err()); + } +} |
