#![doc = include_str!("../README.md")]
mod capabilities;
mod constants;
mod context;
pub use capabilities::CapabilityReply;
pub use context::Context;
use std::io::{self, Write};
use constants::ALPHABET;
pub use osc7501_parser as parser;
pub use parser::{AppId, Kind, Progress, RecordId, State};
#[cfg(feature = "store")]
pub use osc7501_store as store;
use parser::{Action, MAX_MESSAGE, MAX_SEQUENCE, MAX_TITLE};
#[derive(Clone, Copy, Debug)]
pub struct Status<'a> {
action: Action,
id: Option<RecordId<'a>>,
app: Option<AppId<'a>>,
title: Option<&'a str>,
message: Option<&'a str>,
kind: Option<Kind>,
progress: Option<Progress>,
}
impl<'a> Status<'a> {
#[must_use]
pub const fn new(state: State) -> Self {
Self::action(Action::Set(state))
}
#[must_use]
pub const fn clear() -> Self {
Self::action(Action::Clear)
}
const fn action(action: Action) -> Self {
Self {
action,
id: None,
app: None,
title: None,
message: None,
kind: None,
progress: None,
}
}
#[must_use]
pub const fn id(mut self, id: RecordId<'a>) -> Self {
self.id = Some(id);
self
}
#[must_use]
pub const fn app(mut self, app: AppId<'a>) -> Self {
self.app = Some(app);
self
}
#[must_use]
pub const fn title(mut self, title: &'a str) -> Self {
self.title = Some(title);
self
}
#[must_use]
pub const fn message(mut self, message: &'a str) -> Self {
self.message = Some(message);
self
}
#[must_use]
pub const fn kind(mut self, kind: Kind) -> Self {
self.kind = Some(kind);
self
}
#[must_use]
pub const fn progress(mut self, progress: Progress) -> Self {
self.progress = Some(progress);
self
}
pub fn write_to<W: Write + ?Sized>(self, output: &mut W) -> Result<(), Error> {
self.validate()?;
let mut frame = [0; MAX_SEQUENCE];
let mut remaining = frame.as_mut_slice();
remaining.write_all(b"\x1b]7501;state=")?;
remaining.write_all(self.action.as_str().as_bytes())?;
for (key, value) in [
("id", self.id.map(RecordId::as_str)),
("app", self.app.map(AppId::as_str)),
("kind", self.kind.map(Kind::as_str)),
] {
if let Some(value) = value {
write!(remaining, ":{key}={value}")?;
}
}
if let Some(progress) = self.progress {
write!(remaining, ":progress={}", progress.get())?;
}
if let Some(title) = self.title {
remaining.write_all(b":title=")?;
encode(title.as_bytes(), &mut remaining)?;
}
if let Some(message) = self.message {
remaining.write_all(b":msg=")?;
encode(message.as_bytes(), &mut remaining)?;
}
remaining.write_all(b"\x1b\\")?;
let length = MAX_SEQUENCE - remaining.len();
output.write_all(&frame[..length])?;
Ok(())
}
fn validate(self) -> Result<(), Error> {
if self.kind.is_some() && self.action != Action::Set(State::Blocked) {
return Err(Error::IncompatibleField);
}
if self.progress.is_some()
&& !matches!(self.action, Action::Set(State::Working | State::Blocked))
{
return Err(Error::IncompatibleField);
}
for (text, cap) in [(self.title, MAX_TITLE), (self.message, MAX_MESSAGE)] {
if let Some(text) = text {
if text.len() > cap {
return Err(Error::TextTooLong);
}
if text.chars().any(char::is_control) {
return Err(Error::ControlCharacter);
}
}
}
Ok(())
}
}
fn encode(input: &[u8], output: &mut impl Write) -> io::Result<()> {
for chunk in input.chunks(3) {
let a = chunk[0];
let b = chunk.get(1).copied().unwrap_or(0);
let c = chunk.get(2).copied().unwrap_or(0);
let encoded = [
ALPHABET[usize::from(a >> 2)],
ALPHABET[usize::from((a & 3) << 4 | b >> 4)],
if chunk.len() > 1 {
ALPHABET[usize::from((b & 15) << 2 | c >> 6)]
} else {
b'='
},
if chunk.len() > 2 {
ALPHABET[usize::from(c & 63)]
} else {
b'='
},
];
output.write_all(&encoded)?;
}
Ok(())
}
pub fn write_query<W: Write + ?Sized>(output: &mut W) -> io::Result<()> {
output.write_all(b"\x1b]7501;?\x1b\\")
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("status text exceeds its byte cap")]
TextTooLong,
#[error("status text contains a control character")]
ControlCharacter,
#[error("status field is incompatible with its state")]
IncompatibleField,
#[error("{0}")]
Parser(#[from] parser::Error),
#[error("{0}")]
Io(#[from] io::Error),
}