pub const OSC_PREFIX: &[u8] = b"\x1b]9;4;";
pub const TERMINATOR_ST: &[u8] = b"\x1b\\";
pub const TERMINATOR_BEL: &[u8] = b"\x07";
pub const TERMINATOR_C1_ST: &[u8] = b"\x9c";
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
#[repr(u8)]
pub enum ProgressState {
Clear = 0,
#[default]
Normal = 1,
Error = 2,
Indeterminate = 3,
Paused = 4,
}
impl ProgressState {
#[inline]
#[must_use]
pub const fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(Self::Clear),
1 => Some(Self::Normal),
2 => Some(Self::Error),
3 => Some(Self::Indeterminate),
4 => Some(Self::Paused),
_ => None,
}
}
}
impl From<ProgressState> for u8 {
#[inline]
fn from(state: ProgressState) -> Self {
state as Self
}
}
impl TryFrom<u8> for ProgressState {
type Error = u8;
#[inline]
fn try_from(value: u8) -> Result<Self, <Self as TryFrom<u8>>::Error> {
Self::from_u8(value).ok_or(value)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Terminator {
#[default]
St,
Bel,
C1St,
}
impl Terminator {
#[inline]
#[must_use]
pub const fn as_bytes(&self) -> &[u8] {
match self {
Self::St => TERMINATOR_ST,
Self::Bel => TERMINATOR_BEL,
Self::C1St => TERMINATOR_C1_ST,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum WriteError {
BufferTooSmall {
required: usize,
available: usize,
},
PercentOutOfRange(u8),
}
impl core::fmt::Display for WriteError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::BufferTooSmall {
required,
available,
} => write!(
f,
"buffer too small: need {required} bytes, have {available}"
),
Self::PercentOutOfRange(v) => {
write!(f, "percent out of range: {v} (must be 0..=100)")
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for WriteError {}
#[cfg(feature = "std")]
impl From<WriteError> for std::io::Error {
fn from(err: WriteError) -> Self {
Self::other(err)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OscSequence<'a> {
pub state: ProgressState,
pub percent: Option<u8>,
pub label: Option<&'a str>,
pub terminator: Terminator,
}
impl<'a> OscSequence<'a> {
#[must_use]
pub const fn normal(percent: u8) -> Self {
Self {
state: ProgressState::Normal,
percent: Some(percent),
label: None,
terminator: Terminator::St,
}
}
#[must_use]
pub const fn normal_with_label(percent: u8, label: &'a str) -> Self {
Self {
state: ProgressState::Normal,
percent: Some(percent),
label: Some(label),
terminator: Terminator::St,
}
}
#[must_use]
pub const fn indeterminate(label: &'a str) -> Self {
Self {
state: ProgressState::Indeterminate,
percent: None,
label: Some(label),
terminator: Terminator::St,
}
}
#[must_use]
pub const fn clear() -> Self {
Self {
state: ProgressState::Clear,
percent: Some(0),
label: None,
terminator: Terminator::St,
}
}
#[must_use]
pub const fn error(label: &'a str) -> Self {
Self {
state: ProgressState::Error,
percent: None,
label: Some(label),
terminator: Terminator::St,
}
}
#[must_use]
pub fn byte_len(&self) -> usize {
let mut len = OSC_PREFIX.len();
len += 1;
len += 1; if let Some(p) = self.percent {
if p >= 100 {
len += 3;
} else if p >= 10 {
len += 2;
} else {
len += 1;
}
}
len += 1; if let Some(label) = self.label {
len += label.len(); }
len += self.terminator.as_bytes().len();
len
}
pub fn write_to(&self, buf: &mut [u8]) -> Result<usize, WriteError> {
if let Some(p) = self.percent {
if p > 100 {
return Err(WriteError::PercentOutOfRange(p));
}
}
let required = self.byte_len();
if buf.len() < required {
return Err(WriteError::BufferTooSmall {
required,
available: buf.len(),
});
}
let mut pos = 0;
let prefix = OSC_PREFIX;
buf[pos..pos + prefix.len()].copy_from_slice(prefix);
pos += prefix.len();
buf[pos] = b'0' + (self.state as u8);
pos += 1;
buf[pos] = b';';
pos += 1;
if let Some(p) = self.percent {
if p >= 100 {
buf[pos] = b'1';
buf[pos + 1] = b'0';
buf[pos + 2] = b'0';
pos += 3;
} else if p >= 10 {
buf[pos] = b'0' + (p / 10);
buf[pos + 1] = b'0' + (p % 10);
pos += 2;
} else {
buf[pos] = b'0' + p;
pos += 1;
}
}
buf[pos] = b';';
pos += 1;
if let Some(label) = self.label {
let label_bytes = label.as_bytes();
buf[pos..pos + label_bytes.len()].copy_from_slice(label_bytes);
pos += label_bytes.len();
}
let term = self.terminator.as_bytes();
buf[pos..pos + term.len()].copy_from_slice(term);
pos += term.len();
Ok(pos)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normal_progress() {
let seq = OscSequence::normal_with_label(50, "Building");
let mut buf = [0u8; 128];
let n = seq.write_to(&mut buf).unwrap();
assert_eq!(&buf[..n], b"\x1b]9;4;1;50;Building\x1b\\");
}
#[test]
fn clear_progress() {
let seq = OscSequence::clear();
let mut buf = [0u8; 128];
let n = seq.write_to(&mut buf).unwrap();
assert_eq!(&buf[..n], b"\x1b]9;4;0;0;\x1b\\");
}
#[test]
fn indeterminate_progress() {
let seq = OscSequence::indeterminate("Waiting");
let mut buf = [0u8; 128];
let n = seq.write_to(&mut buf).unwrap();
assert_eq!(&buf[..n], b"\x1b]9;4;3;;Waiting\x1b\\");
}
#[test]
fn error_progress() {
let seq = OscSequence::error("Failed");
let mut buf = [0u8; 128];
let n = seq.write_to(&mut buf).unwrap();
assert_eq!(&buf[..n], b"\x1b]9;4;2;;Failed\x1b\\");
}
#[test]
fn bel_terminator() {
let seq = OscSequence {
state: ProgressState::Normal,
percent: Some(99),
label: None,
terminator: Terminator::Bel,
};
let mut buf = [0u8; 128];
let n = seq.write_to(&mut buf).unwrap();
assert_eq!(&buf[..n], b"\x1b]9;4;1;99;\x07");
}
#[test]
fn percent_boundaries() {
let seq = OscSequence::normal(0);
let mut buf = [0u8; 128];
let n = seq.write_to(&mut buf).unwrap();
assert_eq!(&buf[..n], b"\x1b]9;4;1;0;\x1b\\");
let seq = OscSequence::normal(100);
let n = seq.write_to(&mut buf).unwrap();
assert_eq!(&buf[..n], b"\x1b]9;4;1;100;\x1b\\");
}
#[test]
fn percent_out_of_range() {
let seq = OscSequence::normal(101);
let mut buf = [0u8; 128];
assert_eq!(
seq.write_to(&mut buf),
Err(WriteError::PercentOutOfRange(101))
);
}
#[test]
fn buffer_too_small() {
let seq = OscSequence::normal(50);
let mut buf = [0u8; 2];
assert!(matches!(
seq.write_to(&mut buf),
Err(WriteError::BufferTooSmall { .. })
));
}
#[test]
fn all_states_roundtrip() {
for state_val in 0..=4u8 {
let state = ProgressState::from_u8(state_val).unwrap();
assert_eq!(state as u8, state_val);
}
assert!(ProgressState::from_u8(5).is_none());
}
}