1use crate::backend::{Cursor, CursorStyle, DrawCell, Input, Output};
13use crate::color::Style;
14use crate::event::{Event, push_coalesced};
15use crate::grid::{Grid, HasSize, Pos, Size};
16use crate::tile::Tile;
17use alloc::collections::VecDeque;
18use alloc::string::String;
19use core::time::Duration;
20
21pub struct Headless {
25 layers: Grid,
26 composited: Option<Grid>,
27 cursor_visible: bool,
28 cursor_pos: Pos,
29 cursor_style: CursorStyle,
30 event_queue: VecDeque<Event>,
31}
32
33impl Headless {
34 pub const SPACE_GLYPH: char = '·';
43
44 #[must_use]
46 pub fn new(width: u16, height: u16) -> Self {
47 Self {
48 layers: Grid::new(width, height),
49 composited: None,
50 cursor_visible: false,
51 cursor_pos: Pos::default(),
52 cursor_style: CursorStyle::default(),
53 event_queue: VecDeque::new(),
54 }
55 }
56
57 #[must_use]
67 pub fn grid(&self) -> &Grid {
68 self.composited.as_ref().unwrap_or(&self.layers)
69 }
70
71 #[must_use]
79 pub const fn layer_grid(&self) -> &Grid {
80 &self.layers
81 }
82
83 fn recomposite(&mut self) {
88 if self.layers.max_layer() == 0 {
89 return;
90 }
91 let size = self.layers.size();
92 let dst = self
93 .composited
94 .get_or_insert_with(|| Grid::new(size.width(), size.height()));
95 self.layers.flatten_into(dst);
96 }
97
98 #[must_use]
100 pub const fn cursor_visible(&self) -> bool {
101 self.cursor_visible
102 }
103
104 #[must_use]
106 pub const fn cursor_position(&self) -> Pos {
107 self.cursor_pos
108 }
109
110 #[must_use]
113 pub const fn cursor_style(&self) -> CursorStyle {
114 self.cursor_style
115 }
116
117 pub fn push_event(&mut self, event: Event) {
125 push_coalesced(&mut self.event_queue, event);
126 }
127
128 #[must_use]
132 pub fn format_view(&self) -> String {
133 let grid = self.grid();
134 let mut out = String::new();
135 for row in grid.size().to_rect().rows() {
136 for pos in row {
137 let cell = &grid[pos];
138 let (glyph, is_spacer) = Self::display_glyph(cell);
139 out.push(if is_spacer { ' ' } else { glyph });
140 }
141 out.push('\n');
142 }
143 out
144 }
145
146 #[must_use]
163 pub fn format_styled(&self) -> String {
164 let grid = self.grid();
165 let mut out = String::new();
166 for row in grid.size().to_rect().rows() {
167 let mut current: Option<Style> = None;
168 for pos in row {
169 let cell = &grid[pos];
170 let (glyph, is_spacer) = Self::display_glyph(cell);
171 let style = if is_spacer {
172 Style::default()
173 } else {
174 cell.style()
175 };
176 if current != Some(style) {
177 out.push_str("\x1b[0m");
178 crate::color::sgr::push_sgr(&mut out, style);
179 current = Some(style);
180 }
181 out.push(if is_spacer { ' ' } else { glyph });
182 }
183 if current.is_some_and(|s| s != Style::default()) {
184 out.push_str("\x1b[0m");
185 }
186 out.push('\n');
187 }
188 out
189 }
190
191 const fn display_glyph(cell: &Tile) -> (char, bool) {
194 let is_spacer = cell
195 .flags()
196 .contains(crate::tile::TileFlags::WIDE_CHAR_SPACER);
197 let glyph = if cell.glyph() == ' ' {
198 Self::SPACE_GLYPH
199 } else {
200 cell.glyph()
201 };
202 (glyph, is_spacer)
203 }
204}
205
206impl Output for Headless {
207 type Error = core::convert::Infallible;
208
209 fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
210 where
211 I: Iterator<Item = DrawCell<'a>>,
212 {
213 for cell in content {
214 let pos = cell.pos;
215 if let Some(t) = self.layers.tile_mut_or_alloc(cell.layer, pos) {
230 *t = *cell.tile;
231 }
232 let extra = crate::grid::TileExtra {
235 grapheme: cell.grapheme.map(alloc::sync::Arc::from),
236 tint: cell.tint,
237 };
238 self.layers.set_extra(cell.layer, pos.x, pos.y, extra);
239 }
240 self.recomposite();
241 Ok(())
242 }
243
244 fn resize(&mut self, size: Size) {
245 self.layers.resize(size.width(), size.height());
246 if let Some(composited) = self.composited.as_mut() {
250 composited.resize(size.width(), size.height());
251 }
252 }
253
254 fn flush(&mut self) -> Result<(), Self::Error> {
255 Ok(())
256 }
257
258 fn size(&self) -> Size {
259 Size::new(self.layers.width(), self.layers.height())
260 }
261
262 fn clear(&mut self) -> Result<(), Self::Error> {
263 self.layers.clear_all();
264 if let Some(composited) = self.composited.as_mut() {
265 composited.clear_all();
266 }
267 Ok(())
268 }
269}
270
271impl Input for Headless {
272 fn poll_event(&mut self, _timeout: Duration) -> Option<Event> {
273 self.event_queue.pop_front()
274 }
275
276 fn push_event(&mut self, event: Event) {
277 Self::push_event(self, event);
278 }
279}
280
281impl Cursor for Headless {
282 fn set_cursor_visible(&mut self, visible: bool) {
283 self.cursor_visible = visible;
284 }
285
286 fn set_cursor_position(&mut self, position: Pos) {
287 self.cursor_pos = position;
288 }
289
290 fn set_cursor_style(&mut self, style: CursorStyle) {
291 self.cursor_style = style;
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use crate::color::Color;
299
300 #[test]
301 fn test_headless_new() {
302 let backend = Headless::new(80, 25);
303 assert_eq!(backend.grid().width(), 80);
304 assert_eq!(backend.grid().height(), 25);
305 }
306
307 #[test]
308 fn test_headless_cursor_style_defaults_and_records_set_cursor_style() {
309 use crate::backend::CursorStyle;
310
311 let mut backend = Headless::new(10, 10);
312 assert_eq!(backend.cursor_style(), CursorStyle::BlinkingBlock);
313 Cursor::set_cursor_style(&mut backend, CursorStyle::SteadyBar);
314 assert_eq!(backend.cursor_style(), CursorStyle::SteadyBar);
315 }
316
317 #[test]
318 fn test_headless_events() {
319 let mut backend = Headless::new(10, 10);
320 let event = Event::Close;
321 backend.push_event(event);
322 assert_eq!(backend.poll_event(Duration::ZERO), Some(Event::Close));
323 assert_eq!(backend.poll_event(Duration::ZERO), None);
324 }
325
326 fn moved(x: u16) -> Event {
327 use crate::event::{KeyModifiers, MouseEvent, MouseEventKind};
328 Event::Mouse(MouseEvent {
329 kind: MouseEventKind::Moved,
330 position: Pos { x, y: 0 },
331 pixel_position: None,
332 modifiers: KeyModifiers::NONE,
333 })
334 }
335
336 #[test]
340 fn consecutive_moved_events_coalesce_to_one() {
341 let mut backend = Headless::new(10, 10);
342 for x in 0..1_000u16 {
343 backend.push_event(moved(x));
344 }
345 assert_eq!(backend.event_queue.len(), 1);
346 assert_eq!(backend.poll_event(Duration::ZERO), Some(moved(999)));
347 assert_eq!(backend.poll_event(Duration::ZERO), None);
348 }
349
350 #[test]
353 fn non_moved_event_breaks_coalescing() {
354 use crate::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
355 let mut backend = Headless::new(10, 10);
356 backend.push_event(moved(1));
357 backend.push_event(moved(2));
358 backend.push_event(Event::Mouse(MouseEvent {
359 kind: MouseEventKind::Down(MouseButton::Left),
360 position: Pos { x: 2, y: 0 },
361 pixel_position: None,
362 modifiers: KeyModifiers::NONE,
363 }));
364 backend.push_event(moved(3));
365 assert_eq!(backend.event_queue.len(), 3);
366 assert_eq!(backend.poll_event(Duration::ZERO), Some(moved(2)));
367 assert!(matches!(
368 backend.poll_event(Duration::ZERO),
369 Some(Event::Mouse(MouseEvent {
370 kind: MouseEventKind::Down(MouseButton::Left),
371 ..
372 }))
373 ));
374 assert_eq!(backend.poll_event(Duration::ZERO), Some(moved(3)));
375 }
376
377 #[test]
382 fn draw_layers_treats_empty_higher_layer_cells_as_transparent() {
383 let mut backend = Headless::new(3, 1);
384 let a = Tile::new('a', Style::default());
385 let hash = Tile::new('#', Style::default());
386 let blank = Tile::default();
387 backend
388 .draw_layers(
389 [
390 DrawCell::on_layer(0, Pos::new(0, 0), &a),
391 DrawCell::on_layer(1, Pos::new(0, 0), &blank),
393 DrawCell::on_layer(1, Pos::new(1, 0), &blank),
394 DrawCell::on_layer(1, Pos::new(2, 0), &hash),
395 ]
396 .into_iter(),
397 )
398 .expect("draw_layers failed");
399 assert_eq!(backend.format_view(), "a·#\n");
400 }
401
402 #[test]
406 fn draw_layers_reveals_the_layer_below_when_an_occluder_is_erased() {
407 let mut backend = Headless::new(3, 1);
408 let a = Tile::new('a', Style::default());
409 let hash = Tile::new('#', Style::default());
410 let blank = Tile::default();
411 backend
412 .draw_layers(
413 [
414 DrawCell::on_layer(0, Pos::new(0, 0), &a),
415 DrawCell::on_layer(1, Pos::new(0, 0), &hash),
416 ]
417 .into_iter(),
418 )
419 .expect("draw_layers failed");
420 assert_eq!(backend.format_view(), "#··\n", "layer 1 occludes layer 0");
421
422 backend
424 .draw_layers(core::iter::once(DrawCell::on_layer(
425 1,
426 Pos::new(0, 0),
427 &blank,
428 )))
429 .expect("draw_layers failed");
430 assert_eq!(
431 backend.format_view(),
432 "a··\n",
433 "erasing the occluder must restore layer 0's tile, which was never resent"
434 );
435 }
436
437 #[test]
440 fn draw_layers_keeps_an_explicit_space_on_a_higher_layer_opaque() {
441 let mut backend = Headless::new(3, 1);
442 let a = Tile::new('a', Style::default());
443 let space = Tile::new(' ', Style::default());
444 backend
445 .draw_layers(
446 [
447 DrawCell::on_layer(0, Pos::new(0, 0), &a),
448 DrawCell::on_layer(1, Pos::new(0, 0), &space),
449 ]
450 .into_iter(),
451 )
452 .expect("draw_layers failed");
453 assert_eq!(backend.format_view(), "···\n");
454 }
455
456 #[test]
458 fn layer_grid_exposes_the_raw_per_layer_stream() {
459 let mut backend = Headless::new(3, 1);
460 let a = Tile::new('a', Style::default());
461 let hash = Tile::new('#', Style::default());
462 backend
463 .draw_layers(
464 [
465 DrawCell::on_layer(0, Pos::new(0, 0), &a),
466 DrawCell::on_layer(1, Pos::new(0, 0), &hash),
467 ]
468 .into_iter(),
469 )
470 .expect("draw_layers failed");
471 let raw = backend.layer_grid();
472 assert_eq!(raw.tile(0, Pos::new(0, 0)).map(Tile::glyph), Some('a'));
473 assert_eq!(raw.tile(1, Pos::new(0, 0)).map(Tile::glyph), Some('#'));
474 assert_eq!(backend.grid()[Pos::new(0, 0)].glyph(), '#');
475 }
476
477 #[test]
480 fn single_layer_stream_skips_the_composited_grid() {
481 let mut backend = Headless::new(3, 1);
482 let a = Tile::new('a', Style::default());
483 backend
484 .draw_layers(core::iter::once(DrawCell::on_layer(0, Pos::new(0, 0), &a)))
485 .expect("draw_layers failed");
486 assert!(backend.composited.is_none());
487 assert_eq!(backend.format_view(), "a··\n");
488 }
489
490 #[test]
491 fn test_format_view_snapshot() {
492 use crate::terminal::Terminal;
493 let backend = Headless::new(10, 3);
494 let mut term = Terminal::new(backend);
495 term.draw(|s| {
496 s.put((1, 1), 'H', Style::default());
497 s.put((2, 1), 'i', Style::default());
498 })
499 .expect("draw failed");
500 let view = term.backend().format_view();
501 insta::assert_snapshot!(view, @r#"
502 ··········
503 ·Hi·······
504 ··········
505 "#);
506 }
507
508 #[test]
512 fn test_format_view_renders_span_fallback_glyphs() {
513 use crate::terminal::Terminal;
514 let backend = Headless::new(6, 3);
515 let mut term = Terminal::new(backend);
516 term.draw(|s| {
517 s.put_span((1, 0), &["C=", "[]"], Style::default())
518 .expect("span write");
519 })
520 .expect("draw failed");
521 let view = term.backend().format_view();
522 insta::assert_snapshot!(view, @r#"
523 ·C=···
524 ·[]···
525 ······
526 "#);
527 }
528
529 #[test]
530 fn test_format_styled_unstyled_matches_format_view_text() {
531 use crate::terminal::Terminal;
532 let backend = Headless::new(6, 2);
533 let mut term = Terminal::new(backend);
534 term.draw(|s| {
535 s.put((1, 0), 'H', Style::default());
536 })
537 .expect("draw failed");
538 assert_eq!(
540 term.backend().format_styled(),
541 "\x1b[0m·H····\n\x1b[0m······\n"
542 );
543 }
544
545 #[test]
546 fn test_format_styled_emits_fg_and_bg_sgr_on_change() {
547 use crate::terminal::Terminal;
548 let backend = Headless::new(3, 1);
549 let mut term = Terminal::new(backend);
550 term.draw(|s| {
551 let style = Style::new().fg(Color::RED).bg(Color::BLUE);
552 s.put((1, 0), 'x', style);
553 })
554 .expect("draw failed");
555 assert_eq!(
556 term.backend().format_styled(),
557 "\x1b[0m·\x1b[0m\x1b[31;44mx\x1b[0m·\n"
558 );
559 }
560
561 #[test]
562 fn test_format_styled_rgb_and_indexed() {
563 use crate::terminal::Terminal;
564 let backend = Headless::new(2, 1);
565 let mut term = Terminal::new(backend);
566 term.draw(|s| {
567 s.put((0, 0), 'a', Style::new().fg(Color::rgb(1, 2, 3)));
568 s.put((1, 0), 'b', Style::new().bg(Color::Indexed(200)));
569 })
570 .expect("draw failed");
571 assert_eq!(
572 term.backend().format_styled(),
573 "\x1b[0m\x1b[38;2;1;2;3ma\x1b[0m\x1b[48;5;200mb\x1b[0m\n"
574 );
575 }
576
577 #[test]
578 fn test_format_styled_spacer_cells_carry_no_style() {
579 use crate::terminal::Terminal;
580 let backend = Headless::new(4, 1);
581 let mut term = Terminal::new(backend);
582 term.draw(|s| {
583 s.put_span((0, 0), &["[]"], Style::new().fg(Color::GREEN))
584 .expect("span write");
585 })
586 .expect("draw failed");
587 #[cfg(feature = "egc")]
591 {
592 let mut term = Terminal::new(Headless::new(4, 1));
593 term.draw(|s| {
594 s.put((0, 0), 'あ', Style::new().fg(Color::GREEN));
595 })
596 .expect("draw failed");
597 let styled = term.backend().format_styled();
598 assert!(styled.contains("\x1b[32mあ"));
599 assert!(!styled.contains("\x1b[32m "));
602 }
603 }
604}