1use crate::backend::{Cursor, CursorStyle, DrawCell, Input, Output};
15use crate::color::Color;
16use crate::color::Style;
17use crate::event::{Event, coalesces_with};
18use crate::grid::{Grid, Pos, Size};
19use crate::tile::Tile;
20use alloc::collections::VecDeque;
21use alloc::string::String;
22use core::fmt::Write as _;
23use core::time::Duration;
24use ixy::HasSize;
25
26pub struct Headless {
30 layers: Grid,
31 composited: Option<Grid>,
32 cursor_visible: bool,
33 cursor_pos: Pos,
34 cursor_style: CursorStyle,
35 event_queue: VecDeque<Event>,
36}
37
38impl Headless {
39 #[must_use]
41 pub fn new(width: u16, height: u16) -> Self {
42 Self {
43 layers: Grid::new(width, height),
44 composited: None,
45 cursor_visible: false,
46 cursor_pos: Pos::default(),
47 cursor_style: CursorStyle::default(),
48 event_queue: VecDeque::new(),
49 }
50 }
51
52 #[must_use]
62 pub fn grid(&self) -> &Grid {
63 self.composited.as_ref().unwrap_or(&self.layers)
64 }
65
66 #[must_use]
73 pub const fn layer_grid(&self) -> &Grid {
74 &self.layers
75 }
76
77 fn recomposite(&mut self) {
82 if self.layers.max_layer() == 0 {
83 return;
84 }
85 let size = self.layers.size();
86 let dst = self
87 .composited
88 .get_or_insert_with(|| Grid::new(size.width(), size.height()));
89 self.layers.flatten_into(dst);
90 }
91
92 #[must_use]
94 pub const fn cursor_visible(&self) -> bool {
95 self.cursor_visible
96 }
97
98 #[must_use]
100 pub const fn cursor_position(&self) -> Pos {
101 self.cursor_pos
102 }
103
104 #[must_use]
107 pub const fn cursor_style(&self) -> CursorStyle {
108 self.cursor_style
109 }
110
111 pub fn push_event(&mut self, event: Event) {
119 if let Some(back) = self.event_queue.back_mut()
120 && coalesces_with(&event, back)
121 {
122 *back = event;
123 return;
124 }
125 self.event_queue.push_back(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 Self::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 '·'
199 } else {
200 cell.glyph()
201 };
202 (glyph, is_spacer)
203 }
204
205 fn push_sgr(out: &mut String, style: Style) {
212 let mut params = String::new();
213 if let Some(code) = Self::sgr_color(style.foreground(), false) {
214 let _ = write!(params, "{code}");
215 }
216 if let Some(code) = Self::sgr_color(style.background(), true) {
217 if !params.is_empty() {
218 params.push(';');
219 }
220 let _ = write!(params, "{code}");
221 }
222 if !params.is_empty() {
223 let _ = write!(out, "\x1b[{params}m");
224 }
225 }
226
227 fn sgr_color(color: Color, bg: bool) -> Option<String> {
233 match color {
234 Color::Default => None,
235 Color::Ansi(ansi) => {
236 let index = ansi.to_index();
237 let base = match (index < 8, bg) {
238 (true, false) => 30,
239 (true, true) => 40,
240 (false, false) => 90,
241 (false, true) => 100,
242 };
243 Some(alloc::format!("{}", base + index % 8))
244 }
245 Color::Indexed(index) => Some(alloc::format!("{};5;{index}", if bg { 48 } else { 38 })),
246 Color::Rgb { r, g, b } => {
247 Some(alloc::format!("{};2;{r};{g};{b}", if bg { 48 } else { 38 }))
248 }
249 }
250 }
251}
252
253impl Output for Headless {
254 type Error = core::convert::Infallible;
255
256 fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
257 where
258 I: Iterator<Item = DrawCell<'a>>,
259 {
260 for cell in content {
261 let pos = cell.pos;
262 if let Some(t) = self.layers.tile_mut_or_alloc(cell.layer, pos) {
277 *t = *cell.tile;
278 }
279 let extra = crate::grid::TileExtra {
282 grapheme: cell.grapheme.map(alloc::sync::Arc::from),
283 tint: cell.tint,
284 };
285 self.layers.set_extra(cell.layer, pos.x, pos.y, extra);
286 }
287 self.recomposite();
288 Ok(())
289 }
290
291 fn resize(&mut self, size: Size) {
292 self.layers.resize(size.width(), size.height());
293 if let Some(composited) = self.composited.as_mut() {
297 composited.resize(size.width(), size.height());
298 }
299 }
300
301 fn flush(&mut self) -> Result<(), Self::Error> {
302 Ok(())
303 }
304
305 fn size(&self) -> Size {
306 Size::new(self.layers.width(), self.layers.height())
307 }
308
309 fn clear(&mut self) -> Result<(), Self::Error> {
310 self.layers.clear_all();
311 if let Some(composited) = self.composited.as_mut() {
312 composited.clear_all();
313 }
314 Ok(())
315 }
316}
317
318impl Input for Headless {
319 fn poll_event(&mut self, _timeout: Duration) -> Option<Event> {
320 self.event_queue.pop_front()
321 }
322
323 fn push_event(&mut self, event: Event) {
324 Self::push_event(self, event);
325 }
326}
327
328impl Cursor for Headless {
329 fn set_cursor_visible(&mut self, visible: bool) {
330 self.cursor_visible = visible;
331 }
332
333 fn set_cursor_position(&mut self, position: Pos) {
334 self.cursor_pos = position;
335 }
336
337 fn set_cursor_style(&mut self, style: CursorStyle) {
338 self.cursor_style = style;
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn test_headless_new() {
348 let backend = Headless::new(80, 25);
349 assert_eq!(backend.grid().width(), 80);
350 assert_eq!(backend.grid().height(), 25);
351 }
352
353 #[test]
354 fn test_headless_cursor_style_defaults_and_records_set_cursor_style() {
355 use crate::backend::CursorStyle;
356
357 let mut backend = Headless::new(10, 10);
358 assert_eq!(backend.cursor_style(), CursorStyle::BlinkingBlock);
359 Cursor::set_cursor_style(&mut backend, CursorStyle::SteadyBar);
360 assert_eq!(backend.cursor_style(), CursorStyle::SteadyBar);
361 }
362
363 #[test]
364 fn test_headless_events() {
365 let mut backend = Headless::new(10, 10);
366 let event = Event::Close;
367 backend.push_event(event);
368 assert_eq!(backend.poll_event(Duration::ZERO), Some(Event::Close));
369 assert_eq!(backend.poll_event(Duration::ZERO), None);
370 }
371
372 fn moved(x: u16) -> Event {
373 use crate::event::{KeyModifiers, MouseEvent, MouseEventKind};
374 Event::Mouse(MouseEvent {
375 kind: MouseEventKind::Moved,
376 position: Pos { x, y: 0 },
377 pixel_position: None,
378 modifiers: KeyModifiers::NONE,
379 })
380 }
381
382 #[test]
386 fn consecutive_moved_events_coalesce_to_one() {
387 let mut backend = Headless::new(10, 10);
388 for x in 0..1_000u16 {
389 backend.push_event(moved(x));
390 }
391 assert_eq!(backend.event_queue.len(), 1);
392 assert_eq!(backend.poll_event(Duration::ZERO), Some(moved(999)));
393 assert_eq!(backend.poll_event(Duration::ZERO), None);
394 }
395
396 #[test]
399 fn non_moved_event_breaks_coalescing() {
400 use crate::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
401 let mut backend = Headless::new(10, 10);
402 backend.push_event(moved(1));
403 backend.push_event(moved(2));
404 backend.push_event(Event::Mouse(MouseEvent {
405 kind: MouseEventKind::Down(MouseButton::Left),
406 position: Pos { x: 2, y: 0 },
407 pixel_position: None,
408 modifiers: KeyModifiers::NONE,
409 }));
410 backend.push_event(moved(3));
411 assert_eq!(backend.event_queue.len(), 3);
412 assert_eq!(backend.poll_event(Duration::ZERO), Some(moved(2)));
413 assert!(matches!(
414 backend.poll_event(Duration::ZERO),
415 Some(Event::Mouse(MouseEvent {
416 kind: MouseEventKind::Down(MouseButton::Left),
417 ..
418 }))
419 ));
420 assert_eq!(backend.poll_event(Duration::ZERO), Some(moved(3)));
421 }
422
423 #[test]
428 fn draw_layers_treats_empty_higher_layer_cells_as_transparent() {
429 let mut backend = Headless::new(3, 1);
430 let a = Tile::new('a', Style::default());
431 let hash = Tile::new('#', Style::default());
432 let blank = Tile::default();
433 backend
434 .draw_layers(
435 [
436 DrawCell::on_layer(0, Pos::new(0, 0), &a),
437 DrawCell::on_layer(1, Pos::new(0, 0), &blank),
439 DrawCell::on_layer(1, Pos::new(1, 0), &blank),
440 DrawCell::on_layer(1, Pos::new(2, 0), &hash),
441 ]
442 .into_iter(),
443 )
444 .expect("draw_layers failed");
445 assert_eq!(backend.format_view(), "a·#\n");
446 }
447
448 #[test]
452 fn draw_layers_reveals_the_layer_below_when_an_occluder_is_erased() {
453 let mut backend = Headless::new(3, 1);
454 let a = Tile::new('a', Style::default());
455 let hash = Tile::new('#', Style::default());
456 let blank = Tile::default();
457 backend
458 .draw_layers(
459 [
460 DrawCell::on_layer(0, Pos::new(0, 0), &a),
461 DrawCell::on_layer(1, Pos::new(0, 0), &hash),
462 ]
463 .into_iter(),
464 )
465 .expect("draw_layers failed");
466 assert_eq!(backend.format_view(), "#··\n", "layer 1 occludes layer 0");
467
468 backend
470 .draw_layers(core::iter::once(DrawCell::on_layer(
471 1,
472 Pos::new(0, 0),
473 &blank,
474 )))
475 .expect("draw_layers failed");
476 assert_eq!(
477 backend.format_view(),
478 "a··\n",
479 "erasing the occluder must restore layer 0's tile, which was never resent"
480 );
481 }
482
483 #[test]
486 fn draw_layers_keeps_an_explicit_space_on_a_higher_layer_opaque() {
487 let mut backend = Headless::new(3, 1);
488 let a = Tile::new('a', Style::default());
489 let space = Tile::new(' ', Style::default());
490 backend
491 .draw_layers(
492 [
493 DrawCell::on_layer(0, Pos::new(0, 0), &a),
494 DrawCell::on_layer(1, Pos::new(0, 0), &space),
495 ]
496 .into_iter(),
497 )
498 .expect("draw_layers failed");
499 assert_eq!(backend.format_view(), "···\n");
500 }
501
502 #[test]
504 fn layer_grid_exposes_the_raw_per_layer_stream() {
505 let mut backend = Headless::new(3, 1);
506 let a = Tile::new('a', Style::default());
507 let hash = Tile::new('#', Style::default());
508 backend
509 .draw_layers(
510 [
511 DrawCell::on_layer(0, Pos::new(0, 0), &a),
512 DrawCell::on_layer(1, Pos::new(0, 0), &hash),
513 ]
514 .into_iter(),
515 )
516 .expect("draw_layers failed");
517 let raw = backend.layer_grid();
518 assert_eq!(raw.tile(0, Pos::new(0, 0)).map(Tile::glyph), Some('a'));
519 assert_eq!(raw.tile(1, Pos::new(0, 0)).map(Tile::glyph), Some('#'));
520 assert_eq!(backend.grid()[Pos::new(0, 0)].glyph(), '#');
521 }
522
523 #[test]
526 fn single_layer_stream_skips_the_composited_grid() {
527 let mut backend = Headless::new(3, 1);
528 let a = Tile::new('a', Style::default());
529 backend
530 .draw_layers(core::iter::once(DrawCell::on_layer(0, Pos::new(0, 0), &a)))
531 .expect("draw_layers failed");
532 assert!(backend.composited.is_none());
533 assert_eq!(backend.format_view(), "a··\n");
534 }
535
536 #[test]
537 fn test_format_view_snapshot() {
538 use crate::terminal::Terminal;
539 let backend = Headless::new(10, 3);
540 let mut term = Terminal::new(backend);
541 term.draw(|s| {
542 s.put((1, 1), 'H', Style::default());
543 s.put((2, 1), 'i', Style::default());
544 })
545 .expect("draw failed");
546 let view = term.backend().format_view();
547 insta::assert_snapshot!(view, @r#"
548 ··········
549 ·Hi·······
550 ··········
551 "#);
552 }
553
554 #[test]
558 fn test_format_view_renders_span_fallback_glyphs() {
559 use crate::terminal::Terminal;
560 let backend = Headless::new(6, 3);
561 let mut term = Terminal::new(backend);
562 term.draw(|s| {
563 s.put_span((1, 0), &["C=", "[]"], Style::default())
564 .expect("span write");
565 })
566 .expect("draw failed");
567 let view = term.backend().format_view();
568 insta::assert_snapshot!(view, @r#"
569 ·C=···
570 ·[]···
571 ······
572 "#);
573 }
574
575 #[test]
576 fn test_format_styled_unstyled_matches_format_view_text() {
577 use crate::terminal::Terminal;
578 let backend = Headless::new(6, 2);
579 let mut term = Terminal::new(backend);
580 term.draw(|s| {
581 s.put((1, 0), 'H', Style::default());
582 })
583 .expect("draw failed");
584 assert_eq!(
586 term.backend().format_styled(),
587 "\x1b[0m·H····\n\x1b[0m······\n"
588 );
589 }
590
591 #[test]
592 fn test_format_styled_emits_fg_and_bg_sgr_on_change() {
593 use crate::terminal::Terminal;
594 let backend = Headless::new(3, 1);
595 let mut term = Terminal::new(backend);
596 term.draw(|s| {
597 let style = Style::new().fg(Color::RED).bg(Color::BLUE);
598 s.put((1, 0), 'x', style);
599 })
600 .expect("draw failed");
601 assert_eq!(
602 term.backend().format_styled(),
603 "\x1b[0m·\x1b[0m\x1b[31;44mx\x1b[0m·\n"
604 );
605 }
606
607 #[test]
608 fn test_format_styled_rgb_and_indexed() {
609 use crate::terminal::Terminal;
610 let backend = Headless::new(2, 1);
611 let mut term = Terminal::new(backend);
612 term.draw(|s| {
613 s.put(
614 (0, 0),
615 'a',
616 Style::new().fg(Color::Rgb { r: 1, g: 2, b: 3 }),
617 );
618 s.put((1, 0), 'b', Style::new().bg(Color::Indexed(200)));
619 })
620 .expect("draw failed");
621 assert_eq!(
622 term.backend().format_styled(),
623 "\x1b[0m\x1b[38;2;1;2;3ma\x1b[0m\x1b[48;5;200mb\x1b[0m\n"
624 );
625 }
626
627 #[test]
628 fn test_format_styled_spacer_cells_carry_no_style() {
629 use crate::terminal::Terminal;
630 let backend = Headless::new(4, 1);
631 let mut term = Terminal::new(backend);
632 term.draw(|s| {
633 s.put_span((0, 0), &["[]"], Style::new().fg(Color::GREEN))
634 .expect("span write");
635 })
636 .expect("draw failed");
637 #[cfg(feature = "egc")]
641 {
642 let mut term = Terminal::new(Headless::new(4, 1));
643 term.draw(|s| {
644 s.put((0, 0), 'あ', Style::new().fg(Color::GREEN));
645 })
646 .expect("draw failed");
647 let styled = term.backend().format_styled();
648 assert!(styled.contains("\x1b[32mあ"));
649 assert!(!styled.contains("\x1b[32m "));
652 }
653 }
654}