1#![doc = include_str!("../README.md")]
2
3use base64::Engine as _;
4use ratatui_core::{buffer::Buffer, layout::Rect, widgets::Widget};
5use std::borrow::Cow;
6use std::io::{self, Write};
7use std::path::Path;
8
9const PAYLOAD_CHUNK_SIZE: usize = 3072;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ObjectFormat {
14 Obj,
16 Glb,
18 Stl,
20}
21
22impl ObjectFormat {
23 fn as_str(self) -> &'static str {
24 match self {
25 Self::Obj => "obj",
26 Self::Glb => "glb",
27 Self::Stl => "stl",
28 }
29 }
30
31 fn infer(path: &str) -> Self {
32 match Path::new(path)
33 .extension()
34 .and_then(|ext| ext.to_str())
35 .map(|ext| ext.to_ascii_lowercase())
36 .as_deref()
37 {
38 Some("obj") => Self::Obj,
39 Some("stl") => Self::Stl,
40 _ => Self::Glb,
41 }
42 }
43
44 fn payload_name(self) -> &'static str {
45 match self {
46 Self::Obj => "payload.obj",
47 Self::Glb => "payload.glb",
48 Self::Stl => "payload.stl",
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct RattyGraphicSettings<'a> {
56 pub id: u32,
58 pub path: Cow<'a, str>,
60 pub format: ObjectFormat,
62 pub normalize: bool,
64 pub animate: bool,
66 pub scale: f32,
68 pub depth: f32,
70 pub color: Option<[u8; 3]>,
72 pub brightness: f32,
74 pub offset: [f32; 3],
76 pub rotation: [f32; 3],
78 pub scale3: [f32; 3],
80}
81
82impl<'a> RattyGraphicSettings<'a> {
83 pub fn new(path: impl Into<Cow<'a, str>>) -> Self {
85 let path = path.into();
86 Self {
87 id: 1,
88 format: ObjectFormat::infer(&path),
89 path,
90 normalize: true,
91 animate: true,
92 scale: 1.0,
93 depth: 0.0,
94 color: None,
95 brightness: 1.0,
96 offset: [0.0, 0.0, 0.0],
97 rotation: [0.0, 0.0, 0.0],
98 scale3: [1.0, 1.0, 1.0],
99 }
100 }
101
102 pub fn id(mut self, id: u32) -> Self {
104 self.id = id;
105 self
106 }
107
108 pub fn format(mut self, format: ObjectFormat) -> Self {
110 self.format = format;
111 self
112 }
113
114 pub fn normalize(mut self, normalize: bool) -> Self {
125 self.normalize = normalize;
126 self
127 }
128
129 pub fn animate(mut self, animate: bool) -> Self {
131 self.animate = animate;
132 self
133 }
134
135 pub fn scale(mut self, scale: f32) -> Self {
137 self.scale = scale;
138 self
139 }
140
141 pub fn depth(mut self, depth: f32) -> Self {
143 self.depth = depth;
144 self
145 }
146
147 pub fn color(mut self, color: [u8; 3]) -> Self {
149 self.color = Some(color);
150 self
151 }
152
153 pub fn brightness(mut self, brightness: f32) -> Self {
155 self.brightness = brightness;
156 self
157 }
158
159 pub fn offset(mut self, offset: [f32; 3]) -> Self {
161 self.offset = offset;
162 self
163 }
164
165 pub fn rotation(mut self, rotation: [f32; 3]) -> Self {
167 self.rotation = rotation;
168 self
169 }
170
171 pub fn scale3(mut self, scale3: [f32; 3]) -> Self {
173 self.scale3 = scale3;
174 self
175 }
176}
177
178pub struct RattyGraphic<'a> {
180 settings: RattyGraphicSettings<'a>,
181}
182
183impl<'a> RattyGraphic<'a> {
184 pub fn new(settings: RattyGraphicSettings<'a>) -> Self {
186 Self { settings }
187 }
188
189 pub fn settings(&self) -> &RattyGraphicSettings<'a> {
191 &self.settings
192 }
193
194 pub fn settings_mut(&mut self) -> &mut RattyGraphicSettings<'a> {
196 &mut self.settings
197 }
198
199 pub fn register_sequence(&self) -> String {
201 format!(
202 "\x1b_ratty;g;r;id={};fmt={};path={};normalize={}\x1b\\",
203 self.settings.id,
204 self.settings.format.as_str(),
205 self.settings.path,
206 u8::from(self.settings.normalize)
207 )
208 }
209
210 pub fn register_payload_sequences(&self, bytes: &[u8]) -> Vec<String> {
212 self.register_payload_sequences_with_name(bytes, None)
213 }
214
215 pub fn register_payload_sequences_with_name(
217 &self,
218 bytes: &[u8],
219 name: Option<&str>,
220 ) -> Vec<String> {
221 let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
222 let default_name = Path::new(self.settings.path.as_ref())
223 .file_name()
224 .and_then(|name| name.to_str())
225 .filter(|name| !name.is_empty())
226 .unwrap_or_else(|| self.settings.format.payload_name());
227 let name = name.unwrap_or(default_name);
228 let mut sequences = Vec::new();
229
230 for (index, chunk_start) in (0..encoded.len()).step_by(PAYLOAD_CHUNK_SIZE).enumerate() {
231 let chunk_end = (chunk_start + PAYLOAD_CHUNK_SIZE).min(encoded.len());
232 let more = u8::from(chunk_end < encoded.len());
233 let chunk = &encoded[chunk_start..chunk_end];
234 sequences.push(if index == 0 {
235 format!(
236 "\x1b_ratty;g;r;id={};fmt={};source=payload;more={};name={};normalize={};{}\x1b\\",
237 self.settings.id,
238 self.settings.format.as_str(),
239 more,
240 name,
241 u8::from(self.settings.normalize),
242 chunk
243 )
244 } else {
245 format!(
246 "\x1b_ratty;g;r;id={};fmt={};source=payload;more={};{}\x1b\\",
247 self.settings.id,
248 self.settings.format.as_str(),
249 more,
250 chunk
251 )
252 });
253 }
254
255 if sequences.is_empty() {
256 sequences.push(format!(
257 "\x1b_ratty;g;r;id={};fmt={};source=payload;more=0;name={};normalize={};\x1b\\",
258 self.settings.id,
259 self.settings.format.as_str(),
260 name,
261 u8::from(self.settings.normalize),
262 ));
263 }
264
265 sequences
266 }
267
268 pub fn register(&self) -> io::Result<()> {
274 io::stdout().write_all(self.register_sequence().as_bytes())?;
275 io::stdout().flush()
276 }
277
278 pub fn register_payload(&self, bytes: &[u8]) -> io::Result<()> {
284 self.register_payload_with_name(bytes, None)
285 }
286
287 pub fn register_payload_with_name(&self, bytes: &[u8], name: Option<&str>) -> io::Result<()> {
293 let mut stdout = io::stdout();
294 for sequence in self.register_payload_sequences_with_name(bytes, name) {
295 stdout.write_all(sequence.as_bytes())?;
296 }
297 stdout.flush()
298 }
299
300 pub fn place_sequence(&self, area: Rect) -> String {
302 let center_row = area.y.saturating_add(area.height.saturating_sub(1) / 2);
303 let center_col = area.x.saturating_add(area.width.saturating_sub(1) / 2);
304 format!(
305 "\x1b_ratty;g;p;id={};row={};col={};w={};h={};animate={};scale={};depth={};color={};brightness={};px={};py={};pz={};rx={};ry={};rz={};sx={};sy={};sz={}\x1b\\",
306 self.settings.id,
307 center_row,
308 center_col,
309 area.width.max(1),
310 area.height.max(1),
311 u8::from(self.settings.animate),
312 self.settings.scale,
313 self.settings.depth,
314 self.settings
315 .color
316 .map(|[r, g, b]| format!("{r:02x}{g:02x}{b:02x}"))
317 .unwrap_or_else(|| "ffffff".to_string()),
318 self.settings.brightness,
319 self.settings.offset[0],
320 self.settings.offset[1],
321 self.settings.offset[2],
322 self.settings.rotation[0],
323 self.settings.rotation[1],
324 self.settings.rotation[2],
325 self.settings.scale3[0],
326 self.settings.scale3[1],
327 self.settings.scale3[2],
328 )
329 }
330
331 pub fn update_sequence(&self) -> String {
333 format!(
334 "\x1b_ratty;g;u;id={};animate={};scale={};depth={};color={};brightness={};px={};py={};pz={};rx={};ry={};rz={};sx={};sy={};sz={}\x1b\\",
335 self.settings.id,
336 u8::from(self.settings.animate),
337 self.settings.scale,
338 self.settings.depth,
339 self.settings
340 .color
341 .map(|[r, g, b]| format!("{r:02x}{g:02x}{b:02x}"))
342 .unwrap_or_else(|| "ffffff".to_string()),
343 self.settings.brightness,
344 self.settings.offset[0],
345 self.settings.offset[1],
346 self.settings.offset[2],
347 self.settings.rotation[0],
348 self.settings.rotation[1],
349 self.settings.rotation[2],
350 self.settings.scale3[0],
351 self.settings.scale3[1],
352 self.settings.scale3[2],
353 )
354 }
355
356 pub fn delete_sequence(&self) -> String {
358 format!("\x1b_ratty;g;d;id={}\x1b\\", self.settings.id)
359 }
360
361 pub fn delete_all_sequence() -> String {
367 "\x1b_ratty;g;d\x1b\\".to_string()
368 }
369
370 pub fn clear(&self) -> io::Result<()> {
376 io::stdout().write_all(self.delete_sequence().as_bytes())?;
377 io::stdout().flush()
378 }
379
380 pub fn clear_all() -> io::Result<()> {
390 io::stdout().write_all(Self::delete_all_sequence().as_bytes())?;
391 io::stdout().flush()
392 }
393
394 pub fn update(&self) -> io::Result<()> {
400 io::stdout().write_all(self.update_sequence().as_bytes())?;
401 io::stdout().flush()
402 }
403}
404
405impl Widget for &RattyGraphic<'_> {
407 fn render(self, area: Rect, buf: &mut Buffer) {
408 if area.is_empty() {
409 return;
410 }
411
412 let place = self.place_sequence(area);
413
414 if let Some(cell) = buf.cell_mut((area.x, area.y)) {
415 let existing = cell.symbol();
416 let mut symbol = String::with_capacity(place.len() + existing.len());
417 symbol.push_str(&place);
418 symbol.push_str(existing);
419 cell.set_symbol(&symbol);
420 }
421 }
422}