1#![doc = include_str!("../README.md")]
2
3mod capabilities;
4mod constants;
5mod context;
6
7pub use capabilities::CapabilityReply;
8pub use context::Context;
9
10use std::io::{self, Write};
11
12use constants::ALPHABET;
13
14pub use osc7501_parser as parser;
16pub use parser::{AppId, Kind, Progress, RecordId, State};
17
18#[cfg(feature = "store")]
20pub use osc7501_store as store;
21
22use parser::{Action, MAX_MESSAGE, MAX_SEQUENCE, MAX_TITLE};
23
24#[derive(Clone, Copy, Debug)]
26pub struct Status<'a> {
27 action: Action,
28 id: Option<RecordId<'a>>,
29 app: Option<AppId<'a>>,
30 title: Option<&'a str>,
31 message: Option<&'a str>,
32 kind: Option<Kind>,
33 progress: Option<Progress>,
34}
35
36impl<'a> Status<'a> {
37 #[must_use]
39 pub const fn new(state: State) -> Self {
40 Self::action(Action::Set(state))
41 }
42
43 #[must_use]
45 pub const fn clear() -> Self {
46 Self::action(Action::Clear)
47 }
48
49 const fn action(action: Action) -> Self {
50 Self {
51 action,
52 id: None,
53 app: None,
54 title: None,
55 message: None,
56 kind: None,
57 progress: None,
58 }
59 }
60
61 #[must_use]
63 pub const fn id(mut self, id: RecordId<'a>) -> Self {
64 self.id = Some(id);
65 self
66 }
67
68 #[must_use]
70 pub const fn app(mut self, app: AppId<'a>) -> Self {
71 self.app = Some(app);
72 self
73 }
74
75 #[must_use]
77 pub const fn title(mut self, title: &'a str) -> Self {
78 self.title = Some(title);
79 self
80 }
81
82 #[must_use]
84 pub const fn message(mut self, message: &'a str) -> Self {
85 self.message = Some(message);
86 self
87 }
88
89 #[must_use]
91 pub const fn kind(mut self, kind: Kind) -> Self {
92 self.kind = Some(kind);
93 self
94 }
95
96 #[must_use]
98 pub const fn progress(mut self, progress: Progress) -> Self {
99 self.progress = Some(progress);
100 self
101 }
102
103 pub fn write_to<W: Write + ?Sized>(self, output: &mut W) -> Result<(), Error> {
111 self.validate()?;
112 let mut frame = [0; MAX_SEQUENCE];
113 let mut remaining = frame.as_mut_slice();
114 remaining.write_all(b"\x1b]7501;state=")?;
115 remaining.write_all(self.action.as_str().as_bytes())?;
116 for (key, value) in [
117 ("id", self.id.map(RecordId::as_str)),
118 ("app", self.app.map(AppId::as_str)),
119 ("kind", self.kind.map(Kind::as_str)),
120 ] {
121 if let Some(value) = value {
122 write!(remaining, ":{key}={value}")?;
123 }
124 }
125 if let Some(progress) = self.progress {
126 write!(remaining, ":progress={}", progress.get())?;
127 }
128 if let Some(title) = self.title {
129 remaining.write_all(b":title=")?;
130 encode(title.as_bytes(), &mut remaining)?;
131 }
132 if let Some(message) = self.message {
133 remaining.write_all(b":msg=")?;
134 encode(message.as_bytes(), &mut remaining)?;
135 }
136 remaining.write_all(b"\x1b\\")?;
137 let length = MAX_SEQUENCE - remaining.len();
138 output.write_all(&frame[..length])?;
139 Ok(())
140 }
141
142 fn validate(self) -> Result<(), Error> {
143 if self.kind.is_some() && self.action != Action::Set(State::Blocked) {
144 return Err(Error::IncompatibleField);
145 }
146 if self.progress.is_some()
147 && !matches!(self.action, Action::Set(State::Working | State::Blocked))
148 {
149 return Err(Error::IncompatibleField);
150 }
151 for (text, cap) in [(self.title, MAX_TITLE), (self.message, MAX_MESSAGE)] {
152 if let Some(text) = text {
153 if text.len() > cap {
154 return Err(Error::TextTooLong);
155 }
156 if text.chars().any(char::is_control) {
157 return Err(Error::ControlCharacter);
158 }
159 }
160 }
161 Ok(())
162 }
163}
164
165fn encode(input: &[u8], output: &mut impl Write) -> io::Result<()> {
166 for chunk in input.chunks(3) {
167 let a = chunk[0];
168 let b = chunk.get(1).copied().unwrap_or(0);
169 let c = chunk.get(2).copied().unwrap_or(0);
170 let encoded = [
171 ALPHABET[usize::from(a >> 2)],
172 ALPHABET[usize::from((a & 3) << 4 | b >> 4)],
173 if chunk.len() > 1 {
174 ALPHABET[usize::from((b & 15) << 2 | c >> 6)]
175 } else {
176 b'='
177 },
178 if chunk.len() > 2 {
179 ALPHABET[usize::from(c & 63)]
180 } else {
181 b'='
182 },
183 ];
184 output.write_all(&encoded)?;
185 }
186 Ok(())
187}
188
189pub fn write_query<W: Write + ?Sized>(output: &mut W) -> io::Result<()> {
199 output.write_all(b"\x1b]7501;?\x1b\\")
200}
201
202#[derive(Debug, thiserror::Error)]
204#[non_exhaustive]
205pub enum Error {
206 #[error("status text exceeds its byte cap")]
208 TextTooLong,
209 #[error("status text contains a control character")]
211 ControlCharacter,
212 #[error("status field is incompatible with its state")]
214 IncompatibleField,
215 #[error("{0}")]
217 Parser(#[from] parser::Error),
218 #[error("{0}")]
220 Io(#[from] io::Error),
221}