1use crate::caps::{Capabilities, ColorLevel};
3use crate::cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline};
4use crate::color::{ColorAttribute, ColorSpec};
5use crate::escape::csi::{Cursor, Edit, EraseInDisplay, EraseInLine, Sgr, CSI};
6use crate::escape::esc::EscCode;
7use crate::escape::osc::{ITermDimension, ITermFileData, ITermProprietary, OperatingSystemCommand};
8use crate::escape::{Esc, OneBased};
9use crate::image::{ImageDataType, TextureCoordinate};
10use crate::render::RenderTty;
11use crate::surface::{Change, CursorShape, CursorVisibility, LineAttribute, Position};
12use crate::Result;
13use std::io::Write;
14use terminfo::{capability as cap, Capability as TermInfoCapability};
15
16pub struct TerminfoRenderer {
17 caps: Capabilities,
18 current_attr: CellAttributes,
19 pending_attr: Option<CellAttributes>,
20 }
23
24impl TerminfoRenderer {
25 pub fn new(caps: Capabilities) -> Self {
26 Self {
27 caps,
28 current_attr: CellAttributes::default(),
29 pending_attr: None,
30 }
31 }
32
33 fn get_capability<'a, T: TermInfoCapability<'a>>(&'a self) -> Option<T> {
34 self.caps.terminfo_db().and_then(|db| db.get::<T>())
35 }
36
37 fn attr_apply<F: FnOnce(&mut CellAttributes)>(&mut self, func: F) {
38 self.pending_attr = Some(match self.pending_attr.take() {
39 Some(mut attr) => {
40 func(&mut attr);
41 attr
42 }
43 None => {
44 let mut attr = self.current_attr.clone();
45 func(&mut attr);
46 attr
47 }
48 });
49 }
50
51 #[allow(clippy::cognitive_complexity)]
52 fn flush_pending_attr<W: RenderTty + Write>(&mut self, out: &mut W) -> Result<()> {
53 macro_rules! attr_on {
54 ($cap:ident, $sgr:expr) => {{
55 let cap = if self.caps.force_terminfo_render_to_use_ansi_sgr() {
56 None
57 } else {
58 self.get_capability::<cap::$cap>()
59 };
60 if let Some(attr) = cap {
61 attr.expand().to(out.by_ref())?;
62 } else {
63 write!(out, "{}", CSI::Sgr($sgr))?;
64 }
65 }};
66 ($sgr:expr) => {
67 write!(out, "{}", CSI::Sgr($sgr))?;
68 };
69 }
70
71 if let Some(attr) = self.pending_attr.take() {
72 let mut current_foreground = self.current_attr.foreground();
73 let mut current_background = self.current_attr.background();
74
75 if !attr.attribute_bits_equal(&self.current_attr) {
76 current_foreground = ColorAttribute::Default;
78 current_background = ColorAttribute::Default;
79
80 let sgr = if self.caps.force_terminfo_render_to_use_ansi_sgr() {
81 None
82 } else {
83 self.get_capability::<cap::SetAttributes>()
84 };
85 if let Some(sgr) = sgr {
87 sgr.expand()
88 .bold(attr.intensity() == Intensity::Bold)
89 .dim(attr.intensity() == Intensity::Half)
90 .underline(attr.underline() == Underline::Single)
91 .blink(attr.blink() == Blink::Slow)
92 .reverse(attr.reverse())
93 .invisible(attr.invisible())
94 .to(out.by_ref())?;
95 } else {
96 attr_on!(ExitAttributeMode, Sgr::Reset);
97
98 match attr.intensity() {
99 Intensity::Bold => attr_on!(EnterBoldMode, Sgr::Intensity(Intensity::Bold)),
100 Intensity::Half => attr_on!(EnterDimMode, Sgr::Intensity(Intensity::Half)),
101 _ => {}
102 }
103
104 if attr.underline() == Underline::Single {
105 attr_on!(Sgr::Underline(Underline::Single));
106 }
107
108 if attr.blink() == Blink::Slow {
109 attr_on!(Sgr::Blink(Blink::Slow));
110 }
111
112 if attr.reverse() {
113 attr_on!(EnterReverseMode, Sgr::Inverse(true));
114 }
115
116 if attr.invisible() {
117 attr_on!(Sgr::Invisible(true));
118 }
119 }
120
121 if attr.underline() == Underline::Double {
122 attr_on!(Sgr::Underline(Underline::Double));
123 }
124
125 if attr.blink() == Blink::Rapid {
126 attr_on!(Sgr::Blink(Blink::Rapid));
127 }
128
129 if attr.italic() {
130 attr_on!(EnterItalicsMode, Sgr::Italic(true));
131 }
132
133 if attr.strikethrough() {
134 attr_on!(Sgr::StrikeThrough(true));
135 }
136 }
137
138 let has_true_color = self.caps.color_level() == ColorLevel::TrueColor;
139 let terminfo_256_color: i32 = match self.get_capability::<cap::MaxColors>() {
142 Some(cap::MaxColors(n)) => {
143 if n > 256 {
144 0
145 } else {
146 n
147 }
148 }
149 None => 0,
150 };
151
152 if attr.foreground() != current_foreground
153 && self.caps.color_level() != ColorLevel::MonoChrome
154 {
155 match (has_true_color, attr.foreground()) {
156 (true, ColorAttribute::TrueColorWithPaletteFallback(tc, _))
157 | (true, ColorAttribute::TrueColorWithDefaultFallback(tc)) => {
158 write!(
159 out,
160 "{}",
161 CSI::Sgr(Sgr::Foreground(ColorSpec::TrueColor(tc)))
162 )?;
163 }
164 (false, ColorAttribute::TrueColorWithDefaultFallback(_))
165 | (_, ColorAttribute::Default) => {
166 write!(out, "{}", CSI::Sgr(Sgr::Foreground(ColorSpec::Default)))?;
169 }
170 (false, ColorAttribute::TrueColorWithPaletteFallback(_, idx))
171 | (_, ColorAttribute::PaletteIndex(idx)) => {
172 match self.get_capability::<cap::SetAForeground>() {
173 Some(set) if (idx as i32) < terminfo_256_color => {
174 set.expand().color(idx).to(out.by_ref())?;
175 }
176 _ => {
177 write!(
178 out,
179 "{}",
180 CSI::Sgr(Sgr::Foreground(ColorSpec::PaletteIndex(idx)))
181 )?;
182 }
183 }
184 }
185 }
186 }
187
188 if attr.background() != current_background
189 && self.caps.color_level() != ColorLevel::MonoChrome
190 {
191 match (has_true_color, attr.background()) {
192 (true, ColorAttribute::TrueColorWithPaletteFallback(tc, _))
193 | (true, ColorAttribute::TrueColorWithDefaultFallback(tc)) => {
194 write!(
195 out,
196 "{}",
197 CSI::Sgr(Sgr::Background(ColorSpec::TrueColor(tc)))
198 )?;
199 }
200 (false, ColorAttribute::TrueColorWithDefaultFallback(_))
201 | (_, ColorAttribute::Default) => {
202 write!(out, "{}", CSI::Sgr(Sgr::Background(ColorSpec::Default)))?;
205 }
206 (false, ColorAttribute::TrueColorWithPaletteFallback(_, idx))
207 | (_, ColorAttribute::PaletteIndex(idx)) => {
208 match self.get_capability::<cap::SetABackground>() {
209 Some(set) if (idx as i32) < terminfo_256_color => {
210 set.expand().color(idx).to(out.by_ref())?;
211 }
212 _ => {
213 write!(
214 out,
215 "{}",
216 CSI::Sgr(Sgr::Background(ColorSpec::PaletteIndex(idx)))
217 )?;
218 }
219 }
220 }
221 }
222 }
223
224 if self.caps.hyperlinks() {
225 if let Some(link) = attr.hyperlink() {
226 let osc = OperatingSystemCommand::SetHyperlink(Some((**link).clone()));
227 write!(out, "{}", osc)?;
228 } else if self.current_attr.hyperlink().is_some() {
229 let osc = OperatingSystemCommand::SetHyperlink(None);
231 write!(out, "{}", osc)?;
232 }
233 }
234
235 self.current_attr = attr;
236 }
237
238 Ok(())
239 }
240
241 fn cursor_up<W: RenderTty + Write>(&mut self, n: u32, out: &mut W) -> Result<()> {
242 if n > 0 {
243 if let Some(attr) = self.get_capability::<cap::ParmUpCursor>() {
244 attr.expand().count(n).to(out.by_ref())?;
245 } else {
246 write!(out, "{}", CSI::Cursor(Cursor::Up(n)))?;
247 }
248 }
249 Ok(())
250 }
251
252 fn cursor_down<W: RenderTty + Write>(&mut self, n: u32, out: &mut W) -> Result<()> {
253 if n > 0 {
254 if let Some(attr) = self.get_capability::<cap::ParmDownCursor>() {
255 attr.expand().count(n).to(out.by_ref())?;
256 } else {
257 write!(out, "{}", CSI::Cursor(Cursor::Down(n)))?;
258 }
259 }
260 Ok(())
261 }
262
263 fn cursor_y_relative<W: RenderTty + Write>(&mut self, y: isize, out: &mut W) -> Result<()> {
264 if y > 0 {
265 self.cursor_down(y as u32, out)
266 } else {
267 self.cursor_up(-y as u32, out)
268 }
269 }
270
271 fn cursor_left<W: RenderTty + Write>(&mut self, n: u32, out: &mut W) -> Result<()> {
272 if n > 0 {
273 if let Some(attr) = self.get_capability::<cap::ParmLeftCursor>() {
274 attr.expand().count(n).to(out.by_ref())?;
275 } else {
276 write!(out, "{}", CSI::Cursor(Cursor::Left(n)))?;
277 }
278 }
279 Ok(())
280 }
281
282 fn cursor_right<W: RenderTty + Write>(&mut self, n: u32, out: &mut W) -> Result<()> {
283 if n > 0 {
284 if let Some(attr) = self.get_capability::<cap::ParmRightCursor>() {
285 attr.expand().count(n).to(out.by_ref())?;
286 } else {
287 write!(out, "{}", CSI::Cursor(Cursor::Right(n)))?;
288 }
289 }
290 Ok(())
291 }
292
293 fn cursor_x_relative<W: RenderTty + Write>(&mut self, x: isize, out: &mut W) -> Result<()> {
294 if x > 0 {
295 self.cursor_right(x as u32, out)
296 } else {
297 self.cursor_left(-x as u32, out)
298 }
299 }
300
301 fn move_cursor_absolute<W: RenderTty + Write>(
302 &mut self,
303 x: u32,
304 y: u32,
305 out: &mut W,
306 ) -> Result<()> {
307 if x == 0 && y == 0 {
308 if let Some(attr) = self.get_capability::<cap::CursorHome>() {
309 attr.expand().to(out.by_ref())?;
310 return Ok(());
311 }
312 }
313
314 if let Some(attr) = self.get_capability::<cap::CursorAddress>() {
315 attr.expand().x(x).y(y).to(out.by_ref())?;
318 } else {
319 write!(
322 out,
323 "{}",
324 CSI::Cursor(Cursor::Position {
325 line: OneBased::from_zero_based(x),
326 col: OneBased::from_zero_based(y),
327 })
328 )?;
329 }
330 Ok(())
331 }
332
333 #[allow(clippy::cyclomatic_complexity, clippy::cognitive_complexity)]
334 pub fn render_to<W: RenderTty + Write>(
335 &mut self,
336 changes: &[Change],
337 out: &mut W,
338 ) -> Result<()> {
339 macro_rules! record {
340 ($accesor:ident, $value:expr) => {
341 self.attr_apply(|attr| {
342 attr.$accesor(*$value);
343 });
344 };
345 }
346
347 for change in changes {
348 match change {
349 Change::ClearScreen(color) => {
350 let defaults = CellAttributes::default().set_background(*color).clone();
352 if self.current_attr != defaults {
353 self.pending_attr = Some(defaults);
354 self.flush_pending_attr(out)?;
355 }
356 self.pending_attr = None;
357
358 if self.current_attr.background() == ColorAttribute::Default || self.caps.bce()
359 {
360 if let Some(clr) = self.get_capability::<cap::ClearScreen>() {
364 clr.expand().to(out.by_ref())?;
365 } else {
366 self.move_cursor_absolute(0, 0, out)?;
367 write!(
368 out,
369 "{}",
370 CSI::Edit(Edit::EraseInDisplay(EraseInDisplay::EraseDisplay))
371 )?;
372 }
373 } else {
374 self.move_cursor_absolute(0, 0, out)?;
377
378 let (cols, rows) = out.get_size_in_cells()?;
379 let num_spaces = cols * rows;
380 let mut buf = Vec::with_capacity(num_spaces);
381 buf.resize(num_spaces, b' ');
382 out.write_all(buf.as_slice())?;
383 }
384 }
385 Change::ClearToEndOfLine(color) => {
386 let defaults = CellAttributes::default().set_background(*color).clone();
388 if self.current_attr != defaults {
389 self.pending_attr = Some(defaults);
390 self.flush_pending_attr(out)?;
391 }
392 self.pending_attr = None;
393
394 if let Some(clr) = self.get_capability::<cap::ClrEol>() {
398 clr.expand().to(out.by_ref())?;
399 } else {
400 write!(
401 out,
402 "{}",
403 CSI::Edit(Edit::EraseInLine(EraseInLine::EraseToEndOfLine))
404 )?;
405 }
406 }
407 Change::ClearToEndOfScreen(color) => {
408 let defaults = CellAttributes::default().set_background(*color).clone();
410 if self.current_attr != defaults {
411 self.pending_attr = Some(defaults);
412 self.flush_pending_attr(out)?;
413 }
414 self.pending_attr = None;
415
416 if let Some(clr) = self.get_capability::<cap::ClrEos>() {
420 clr.expand().to(out.by_ref())?;
421 } else {
422 write!(
423 out,
424 "{}",
425 CSI::Edit(Edit::EraseInDisplay(EraseInDisplay::EraseToEndOfDisplay))
426 )?;
427 }
428 }
429 Change::Attribute(AttributeChange::Intensity(value)) => {
430 record!(set_intensity, value);
431 }
432 Change::Attribute(AttributeChange::Italic(value)) => {
433 record!(set_italic, value);
434 }
435 Change::Attribute(AttributeChange::Reverse(value)) => {
436 record!(set_reverse, value);
437 }
438 Change::Attribute(AttributeChange::StrikeThrough(value)) => {
439 record!(set_strikethrough, value);
440 }
441 Change::Attribute(AttributeChange::Blink(value)) => {
442 record!(set_blink, value);
443 }
444 Change::Attribute(AttributeChange::Invisible(value)) => {
445 record!(set_invisible, value);
446 }
447 Change::Attribute(AttributeChange::Underline(value)) => {
448 record!(set_underline, value);
449 }
450 Change::Attribute(AttributeChange::Foreground(col)) => {
451 self.attr_apply(|attr| {
452 attr.set_foreground(*col);
453 });
454 }
455 Change::Attribute(AttributeChange::Background(col)) => {
456 self.attr_apply(|attr| {
457 attr.set_background(*col);
458 });
459 }
460 Change::Attribute(AttributeChange::Hyperlink(link)) => {
461 self.attr_apply(|attr| {
462 attr.set_hyperlink(link.clone());
463 });
464 }
465 Change::AllAttributes(all) => {
466 self.pending_attr = Some(all.clone());
467 }
468 Change::Text(text) => {
469 self.flush_pending_attr(out)?;
470 out.by_ref().write_all(text.as_bytes())?;
471 }
472
473 Change::CursorPosition { x, y } => {
474 match (x, y) {
478 (Position::Relative(0), Position::Relative(0)) => {}
479 (Position::Absolute(0), Position::Relative(1)) => {
480 out.by_ref().write_all(b"\r\n")?;
481 }
482 (Position::Absolute(x), Position::Relative(y)) => {
483 self.cursor_y_relative(*y, out)?;
484 out.by_ref().write_all(b"\r")?;
485 self.cursor_right(*x as u32, out)?;
486 }
487
488 (Position::Relative(x), Position::EndRelative(y)) => {
489 let (_cols, rows) = out.get_size_in_cells()?;
490 self.cursor_up(rows as u32, out)?;
491 self.cursor_down(rows.saturating_sub(y + 1) as u32, out)?;
492 self.cursor_x_relative(*x, out)?;
493 }
494 (Position::Relative(x), Position::Relative(y)) => {
495 self.cursor_y_relative(*y, out)?;
496 self.cursor_x_relative(*x, out)?;
497 }
498 (Position::EndRelative(x), Position::Relative(y)) => {
499 self.cursor_y_relative(*y, out)?;
500 let (cols, _rows) = out.get_size_in_cells()?;
501 out.by_ref().write_all(b"\r")?;
502 self.cursor_right(cols.saturating_sub(x + 1) as u32, out)?;
503 }
504
505 (Position::Absolute(x), Position::Absolute(y)) => {
506 self.move_cursor_absolute(*x as u32, *y as u32, out)?;
507 }
508 (Position::Absolute(x), Position::EndRelative(y)) => {
509 let (_cols, rows) = out.get_size_in_cells()?;
510 self.move_cursor_absolute(
511 *x as u32,
512 rows.saturating_sub(y + 1) as u32,
513 out,
514 )?;
515 }
516 (Position::EndRelative(x), Position::EndRelative(y)) => {
517 let (cols, rows) = out.get_size_in_cells()?;
518 self.move_cursor_absolute(
519 cols.saturating_sub(x + 1) as u32,
520 rows.saturating_sub(y + 1) as u32,
521 out,
522 )?;
523 }
524
525 (Position::Relative(x), Position::Absolute(y)) => {
526 let (_cols, rows) = out.get_size_in_cells()?;
527 self.cursor_up(rows as u32, out)?;
528 self.cursor_down(*y as u32, out)?;
529 self.cursor_x_relative(*x, out)?;
530 }
531
532 (Position::EndRelative(x), Position::Absolute(y)) => {
533 let (cols, _rows) = out.get_size_in_cells()?;
534 self.move_cursor_absolute(
535 cols.saturating_sub(x + 1) as u32,
536 *y as u32,
537 out,
538 )?;
539 }
540 }
541 }
542
543 Change::CursorColor(_color) => {
544 }
547 Change::CursorShape(shape) => match shape {
548 CursorShape::Default => {
549 if let Some(normal) = self.get_capability::<cap::CursorNormal>() {
550 normal.expand().to(out.by_ref())?;
551 }
552 if let Some(reset) = self.get_capability::<cap::ResetCursorStyle>() {
553 reset.expand().to(out.by_ref())?;
554 }
555 }
556 _ => {
557 let param = match shape {
558 CursorShape::Default => unreachable!(),
559 CursorShape::BlinkingBlock => 1,
560 CursorShape::SteadyBlock => 2,
561 CursorShape::BlinkingUnderline => 3,
562 CursorShape::SteadyUnderline => 4,
563 CursorShape::BlinkingBar => 5,
564 CursorShape::SteadyBar => 6,
565 };
566 if let Some(set) = self.get_capability::<cap::SetCursorStyle>() {
567 set.expand().kind(param).to(out.by_ref())?;
568 }
569 }
570 },
571 Change::CursorVisibility(visibility) => match visibility {
572 CursorVisibility::Visible => {
573 if let Some(show) = self.get_capability::<cap::CursorNormal>() {
574 show.expand().to(out.by_ref())?;
575 }
576 }
577 CursorVisibility::Hidden => {
578 if let Some(hide) = self.get_capability::<cap::CursorInvisible>() {
579 hide.expand().to(out.by_ref())?;
580 }
581 }
582 },
583 Change::Image(image) => {
584 if self.caps.iterm2_image() {
585 let data = if image.top_left == TextureCoordinate::new_f32(0.0, 0.0)
586 && image.bottom_right == TextureCoordinate::new_f32(1.0, 1.0)
587 {
588 match &*image.image.data() {
591 ImageDataType::EncodedFile(data) => data.to_vec(),
592 ImageDataType::EncodedLease(lease) => lease.get_data()?,
593 ImageDataType::AnimRgba8 { .. } | ImageDataType::Rgba8 { .. } => {
594 unimplemented!()
595 }
596 }
597 } else {
598 unimplemented!();
601 };
602
603 let file = ITermFileData {
604 name: None,
605 size: Some(data.len()),
606 width: ITermDimension::Cells(image.width as i64),
607 height: ITermDimension::Cells(image.height as i64),
608 preserve_aspect_ratio: true,
609 inline: true,
610 do_not_move_cursor: false,
611 data,
612 };
613
614 let osc = OperatingSystemCommand::ITermProprietary(ITermProprietary::File(
615 Box::new(file),
616 ));
617
618 write!(out, "{}", osc)?;
619
620 } else {
622 for y in 0..image.height {
624 for _ in 0..image.width {
625 write!(out, " ")?;
626 }
627
628 if y != image.height - 1 {
629 writeln!(out)?;
630 self.cursor_left(image.width as u32, out)?;
631 }
632 }
633 self.cursor_up(image.height as u32, out)?;
634 }
635 }
636 Change::ScrollRegionUp {
637 first_row,
638 region_size,
639 scroll_count,
640 } => {
641 if *region_size > 0 {
642 if let Some(csr) = self.get_capability::<cap::ChangeScrollRegion>() {
643 let top = *first_row as u32;
644 let bottom = (*first_row + *region_size - 1) as u32;
645 let scroll_count = *scroll_count as u32;
646 csr.expand().top(top).bottom(bottom).to(out.by_ref())?;
647 if scroll_count > 0 {
648 if let Some(scroll) = self.get_capability::<cap::ParmIndex>() {
649 scroll.expand().count(scroll_count).to(out.by_ref())?
650 } else {
651 let scroll = self.get_capability::<cap::ScrollForward>();
652 let set_position = self.get_capability::<cap::CursorAddress>();
653 if let (Some(scroll), Some(set_position)) =
654 (scroll, set_position)
655 {
656 set_position.expand().x(0).y(bottom).to(out.by_ref())?;
657 for _ in 0..scroll_count {
658 scroll.expand().to(out.by_ref())?
659 }
660 }
661 }
662 }
663 }
664 }
665 }
666 Change::ScrollRegionDown {
667 first_row,
668 region_size,
669 scroll_count,
670 } => {
671 if *region_size > 0 {
672 if let Some(csr) = self.get_capability::<cap::ChangeScrollRegion>() {
673 let top = *first_row as u32;
674 let bottom = (*first_row + *region_size - 1) as u32;
675 let scroll_count = *scroll_count as u32;
676 csr.expand().top(top).bottom(bottom).to(out.by_ref())?;
677 if scroll_count > 0 {
678 if let Some(scroll) = self.get_capability::<cap::ParmRindex>() {
679 scroll.expand().count(scroll_count).to(out.by_ref())?
680 } else {
681 let scroll = self.get_capability::<cap::ScrollReverse>();
682 let set_position = self.get_capability::<cap::CursorAddress>();
683 if let (Some(scroll), Some(set_position)) =
684 (scroll, set_position)
685 {
686 set_position.expand().x(0).y(top).to(out.by_ref())?;
687 for _ in 0..scroll_count {
688 scroll.expand().to(out.by_ref())?
689 }
690 }
691 }
692 }
693 }
694 }
695 }
696
697 Change::Title(text) => {
698 let osc = OperatingSystemCommand::SetWindowTitle(text.to_string());
699 write!(out, "{}", osc)?;
700 }
701
702 Change::LineAttribute(attr) => {
703 let esc = Esc::Code(match attr {
704 LineAttribute::DoubleHeightTopHalfLine => {
705 EscCode::DecDoubleHeightTopHalfLine
706 }
707 LineAttribute::DoubleHeightBottomHalfLine => {
708 EscCode::DecDoubleHeightBottomHalfLine
709 }
710 LineAttribute::DoubleWidthLine => EscCode::DecDoubleWidthLine,
711 LineAttribute::SingleWidthLine => EscCode::DecSingleWidthLine,
712 });
713 write!(out, "{esc}")?;
714 }
715 }
716 }
717
718 self.flush_pending_attr(out)?;
719 out.flush()?;
720 Ok(())
721 }
722}
723
724#[cfg(all(test, unix))]
725mod test {
726 use super::*;
727 use crate::bail;
728 use crate::caps::ProbeHints;
729 use crate::color::{AnsiColor, ColorAttribute};
730 use crate::escape::parser::Parser;
731 use crate::escape::{Action, Esc, EscCode};
732 use crate::input::InputEvent;
733 use crate::terminal::unix::{Purge, SetAttributeWhen, UnixTty};
734 use crate::terminal::{cast, ScreenSize, Terminal, TerminalWaker};
735 use libc::winsize;
736 use std::io::{Error as IoError, ErrorKind, Read, Result as IoResult, Write};
737 use std::mem;
738 use std::time::Duration;
739 use terminfo;
740 use termios::Termios;
741
742 fn xterm_terminfo() -> Capabilities {
744 xterm_terminfo_with_hints(ProbeHints::default())
745 }
746
747 fn xterm_terminfo_with_hints(hints: ProbeHints) -> Capabilities {
748 let data = include_bytes!("../../data/xterm-256color");
751 Capabilities::new_with_hints(hints.terminfo_db(Some(
752 terminfo::Database::from_buffer(data.as_ref()).unwrap(),
753 )))
754 .unwrap()
755 }
756
757 fn no_terminfo_all_enabled() -> Capabilities {
758 Capabilities::new_with_hints(ProbeHints::default().color_level(Some(ColorLevel::TrueColor)))
759 .unwrap()
760 }
761
762 struct FakeTty {
763 buf: Vec<u8>,
764 size: winsize,
765 termios: Termios,
766 }
767
768 impl FakeTty {
769 fn new_with_size(width: usize, height: usize) -> Self {
770 let size = winsize {
771 ws_col: cast(width).unwrap(),
772 ws_row: cast(height).unwrap(),
773 ws_xpixel: 0,
774 ws_ypixel: 0,
775 };
776 let buf = Vec::new();
777 Self {
778 size,
779 buf,
780 termios: unsafe { mem::zeroed() },
781 }
782 }
783 }
784 impl RenderTty for FakeTty {
785 fn get_size_in_cells(&mut self) -> Result<(usize, usize)> {
786 Ok((self.size.ws_col as usize, self.size.ws_row as usize))
787 }
788 }
789
790 impl UnixTty for FakeTty {
791 fn get_size(&mut self) -> Result<winsize> {
792 Ok(self.size.clone())
793 }
794 fn set_size(&mut self, size: winsize) -> Result<()> {
795 self.size = size.clone();
796 Ok(())
797 }
798 fn get_termios(&mut self) -> Result<Termios> {
799 Ok(self.termios.clone())
800 }
801 fn set_termios(&mut self, termios: &Termios, _when: SetAttributeWhen) -> Result<()> {
802 self.termios = termios.clone();
803 Ok(())
804 }
805 fn drain(&mut self) -> Result<()> {
807 Ok(())
808 }
809 fn purge(&mut self, _purge: Purge) -> Result<()> {
810 Ok(())
811 }
812 }
813
814 impl Read for FakeTty {
815 fn read(&mut self, _buf: &mut [u8]) -> std::result::Result<usize, IoError> {
816 Err(IoError::new(ErrorKind::Other, "not implemented"))
817 }
818 }
819 impl Write for FakeTty {
820 fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
821 self.buf.write(buf)
822 }
823
824 fn flush(&mut self) -> IoResult<()> {
825 self.buf.flush()
826 }
827 }
828
829 struct FakeTerm {
830 write: FakeTty,
831 renderer: TerminfoRenderer,
832 }
833
834 impl FakeTerm {
835 fn new(caps: Capabilities) -> Self {
836 Self::new_with_size(caps, 80, 24)
837 }
838
839 fn new_with_size(caps: Capabilities, width: usize, height: usize) -> Self {
840 let write = FakeTty::new_with_size(width, height);
841 let renderer = TerminfoRenderer::new(caps);
842 Self { write, renderer }
843 }
844
845 fn parse(&self) -> Vec<Action> {
846 let mut p = Parser::new();
847 p.parse_as_vec(&self.write.buf)
848 }
849 }
850
851 impl Terminal for FakeTerm {
852 fn set_raw_mode(&mut self) -> Result<()> {
853 bail!("not implemented");
854 }
855
856 fn set_cooked_mode(&mut self) -> Result<()> {
857 bail!("not implemented");
858 }
859
860 fn enter_alternate_screen(&mut self) -> Result<()> {
861 bail!("not implemented");
862 }
863
864 fn exit_alternate_screen(&mut self) -> Result<()> {
865 bail!("not implemented");
866 }
867
868 fn render(&mut self, changes: &[Change]) -> Result<()> {
869 self.renderer.render_to(changes, &mut self.write)
870 }
871
872 fn get_screen_size(&mut self) -> Result<ScreenSize> {
873 let size = self.write.get_size()?;
874 Ok(ScreenSize {
875 rows: cast(size.ws_row)?,
876 cols: cast(size.ws_col)?,
877 xpixel: cast(size.ws_xpixel)?,
878 ypixel: cast(size.ws_ypixel)?,
879 })
880 }
881
882 fn set_screen_size(&mut self, size: ScreenSize) -> Result<()> {
883 let size = winsize {
884 ws_row: cast(size.rows)?,
885 ws_col: cast(size.cols)?,
886 ws_xpixel: cast(size.xpixel)?,
887 ws_ypixel: cast(size.ypixel)?,
888 };
889
890 self.write.set_size(size)
891 }
892
893 fn flush(&mut self) -> Result<()> {
894 Ok(())
895 }
896
897 fn poll_input(&mut self, _wait: Option<Duration>) -> Result<Option<InputEvent>> {
898 bail!("not implemented");
899 }
900
901 fn waker(&self) -> TerminalWaker {
902 unimplemented!();
903 }
904 }
905
906 #[test]
907 fn empty_render() {
908 let mut out = FakeTerm::new(xterm_terminfo());
909 out.render(&[]).unwrap();
910 assert_eq!("", String::from_utf8(out.write.buf).unwrap());
911 assert_eq!(out.renderer.current_attr, CellAttributes::default());
912 }
913
914 #[test]
915 fn basic_text() {
916 let mut out = FakeTerm::new(xterm_terminfo());
917 out.render(&[Change::Text("foo".into())]).unwrap();
918 assert_eq!("foo", String::from_utf8(out.write.buf).unwrap());
919 assert_eq!(out.renderer.current_attr, CellAttributes::default());
920 }
921
922 #[test]
923 fn bold_text() {
924 let mut out = FakeTerm::new(xterm_terminfo());
925 out.render(&[
926 Change::Text("not ".into()),
927 Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
928 Change::Text("foo".into()),
929 ])
930 .unwrap();
931
932 let result = out.parse();
933 assert_eq!(
934 result,
935 vec![
936 Action::Print('n'),
937 Action::Print('o'),
938 Action::Print('t'),
939 Action::Print(' '),
940 Action::Esc(Esc::Code(EscCode::AsciiCharacterSetG0)),
941 Action::CSI(CSI::Sgr(Sgr::Reset)),
942 Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))),
943 Action::Print('f'),
944 Action::Print('o'),
945 Action::Print('o'),
946 ]
947 );
948
949 assert_eq!(
950 out.renderer.current_attr,
951 CellAttributes::default()
952 .set_intensity(Intensity::Bold)
953 .clone()
954 );
955 }
956
957 #[test]
958 fn bold_text_force_ansi_sgr() {
960 let mut out = FakeTerm::new(xterm_terminfo_with_hints(
961 ProbeHints::default().force_terminfo_render_to_use_ansi_sgr(Some(true)),
962 ));
963 out.render(&[
964 Change::Text("not ".into()),
965 AttributeChange::Intensity(Intensity::Bold).into(),
966 Change::Text("foo".into()),
967 ])
968 .unwrap();
969
970 let result = out.parse();
971 assert_eq!(
972 result,
973 vec![
974 Action::Print('n'),
976 Action::Print('o'),
977 Action::Print('t'),
978 Action::Print(' '),
979 Action::CSI(CSI::Sgr(Sgr::Reset)),
980 Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))),
981 Action::Print('f'),
982 Action::Print('o'),
983 Action::Print('o'),
984 ],
985 );
986 }
987
988 #[test]
989 fn clear_screen() {
990 let mut out = FakeTerm::new_with_size(xterm_terminfo(), 4, 3);
991 out.render(&[Change::ClearScreen(ColorAttribute::default())])
992 .unwrap();
993
994 let result = out.parse();
995 assert_eq!(
996 result,
997 vec![
998 Action::CSI(CSI::Cursor(Cursor::Position {
999 line: OneBased::new(1),
1000 col: OneBased::new(1)
1001 })),
1002 Action::CSI(CSI::Edit(Edit::EraseInDisplay(
1003 EraseInDisplay::EraseDisplay,
1004 ))),
1005 ]
1006 );
1007
1008 assert_eq!(out.renderer.current_attr, CellAttributes::default());
1009 }
1010
1011 #[test]
1012 fn clear_screen_bce() {
1013 let mut out = FakeTerm::new_with_size(xterm_terminfo(), 4, 3);
1014 out.render(&[Change::ClearScreen(AnsiColor::Maroon.into())])
1015 .unwrap();
1016
1017 let result = out.parse();
1018 assert_eq!(
1019 result,
1020 vec![
1021 Action::CSI(CSI::Sgr(Sgr::Background(AnsiColor::Maroon.into()))),
1022 Action::CSI(CSI::Cursor(Cursor::Position {
1023 line: OneBased::new(1),
1024 col: OneBased::new(1)
1025 })),
1026 Action::CSI(CSI::Edit(Edit::EraseInDisplay(
1027 EraseInDisplay::EraseDisplay,
1028 ))),
1029 ]
1030 );
1031
1032 assert_eq!(
1033 out.renderer.current_attr,
1034 CellAttributes::default()
1035 .set_background(AnsiColor::Maroon)
1036 .clone()
1037 );
1038 }
1039
1040 #[test]
1041 fn clear_screen_no_terminfo() {
1042 let mut out = FakeTerm::new_with_size(no_terminfo_all_enabled(), 4, 3);
1043 out.render(&[Change::ClearScreen(ColorAttribute::default())])
1044 .unwrap();
1045
1046 let result = out.parse();
1047 assert_eq!(
1048 result,
1049 vec![
1050 Action::CSI(CSI::Cursor(Cursor::Position {
1051 line: OneBased::new(1),
1052 col: OneBased::new(1)
1053 })),
1054 Action::CSI(CSI::Edit(Edit::EraseInDisplay(
1055 EraseInDisplay::EraseDisplay,
1056 ))),
1057 ]
1058 );
1059
1060 assert_eq!(out.renderer.current_attr, CellAttributes::default());
1061 }
1062
1063 #[test]
1064 fn clear_screen_bce_no_terminfo() {
1065 let mut out = FakeTerm::new_with_size(no_terminfo_all_enabled(), 4, 3);
1066 out.render(&[Change::ClearScreen(AnsiColor::Maroon.into())])
1067 .unwrap();
1068
1069 let result = out.parse();
1070 assert_eq!(
1071 result,
1072 vec![
1073 Action::CSI(CSI::Sgr(Sgr::Background(AnsiColor::Maroon.into()))),
1074 Action::CSI(CSI::Cursor(Cursor::Position {
1075 line: OneBased::new(1),
1076 col: OneBased::new(1)
1077 })),
1078 Action::Print(' '),
1081 Action::Print(' '),
1082 Action::Print(' '),
1083 Action::Print(' '),
1084 Action::Print(' '),
1085 Action::Print(' '),
1086 Action::Print(' '),
1087 Action::Print(' '),
1088 Action::Print(' '),
1089 Action::Print(' '),
1090 Action::Print(' '),
1091 Action::Print(' '),
1092 ]
1093 );
1094
1095 assert_eq!(
1096 out.renderer.current_attr,
1097 CellAttributes::default()
1098 .set_background(AnsiColor::Maroon)
1099 .clone()
1100 );
1101 }
1102
1103 #[test]
1104 fn bold_text_no_terminfo() {
1105 let mut out = FakeTerm::new(no_terminfo_all_enabled());
1106 out.render(&[
1107 Change::Text("not ".into()),
1108 Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
1109 Change::Text("foo".into()),
1110 ])
1111 .unwrap();
1112
1113 let result = out.parse();
1114 assert_eq!(
1115 result,
1116 vec![
1117 Action::Print('n'),
1118 Action::Print('o'),
1119 Action::Print('t'),
1120 Action::Print(' '),
1121 Action::CSI(CSI::Sgr(Sgr::Reset)),
1122 Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))),
1123 Action::Print('f'),
1124 Action::Print('o'),
1125 Action::Print('o'),
1126 ]
1127 );
1128
1129 assert_eq!(
1130 out.renderer.current_attr,
1131 CellAttributes::default()
1132 .set_intensity(Intensity::Bold)
1133 .clone()
1134 );
1135 }
1136
1137 #[test]
1138 fn red_bold_text() {
1139 let mut out = FakeTerm::new(xterm_terminfo());
1140 out.render(&[
1141 Change::Attribute(AttributeChange::Foreground(AnsiColor::Maroon.into())),
1142 Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
1143 Change::Text("red".into()),
1144 Change::Attribute(AttributeChange::Foreground(AnsiColor::Red.into())),
1145 ])
1146 .unwrap();
1147
1148 let result = out.parse();
1149 assert_eq!(
1150 result,
1151 vec![
1152 Action::Esc(Esc::Code(EscCode::AsciiCharacterSetG0)),
1153 Action::CSI(CSI::Sgr(Sgr::Reset)),
1154 Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))),
1156 Action::CSI(CSI::Sgr(Sgr::Foreground(AnsiColor::Maroon.into()))),
1157 Action::Print('r'),
1158 Action::Print('e'),
1159 Action::Print('d'),
1160 Action::CSI(CSI::Sgr(Sgr::Foreground(AnsiColor::Red.into()))),
1161 ]
1162 );
1163
1164 assert_eq!(
1165 out.renderer.current_attr,
1166 CellAttributes::default()
1167 .set_intensity(Intensity::Bold)
1168 .set_foreground(AnsiColor::Red)
1169 .clone()
1170 );
1171 }
1172
1173 #[test]
1174 fn red_bold_text_no_terminfo() {
1175 let mut out = FakeTerm::new(no_terminfo_all_enabled());
1176 out.render(&[
1177 Change::Attribute(AttributeChange::Foreground(AnsiColor::Maroon.into())),
1178 Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
1179 Change::Text("red".into()),
1180 Change::Attribute(AttributeChange::Foreground(AnsiColor::Red.into())),
1181 ])
1182 .unwrap();
1183
1184 let result = out.parse();
1185 assert_eq!(
1186 result,
1187 vec![
1188 Action::CSI(CSI::Sgr(Sgr::Reset)),
1189 Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))),
1191 Action::CSI(CSI::Sgr(Sgr::Foreground(AnsiColor::Maroon.into()))),
1192 Action::Print('r'),
1193 Action::Print('e'),
1194 Action::Print('d'),
1195 Action::CSI(CSI::Sgr(Sgr::Foreground(AnsiColor::Red.into()))),
1196 ]
1197 );
1198
1199 assert_eq!(
1200 out.renderer.current_attr,
1201 CellAttributes::default()
1202 .set_intensity(Intensity::Bold)
1203 .set_foreground(AnsiColor::Red)
1204 .clone()
1205 );
1206 }
1207
1208 #[test]
1209 fn color_after_attribute_change() {
1210 let mut out = FakeTerm::new(xterm_terminfo());
1211 out.render(&[
1212 Change::Attribute(AttributeChange::Foreground(AnsiColor::Maroon.into())),
1213 Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
1214 Change::Text("red".into()),
1215 Change::Attribute(AttributeChange::Intensity(Intensity::Normal)),
1216 Change::Text("2".into()),
1217 ])
1218 .unwrap();
1219
1220 let result = out.parse();
1221 assert_eq!(
1222 result,
1223 vec![
1224 Action::Esc(Esc::Code(EscCode::AsciiCharacterSetG0)),
1225 Action::CSI(CSI::Sgr(Sgr::Reset)),
1226 Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))),
1228 Action::CSI(CSI::Sgr(Sgr::Foreground(AnsiColor::Maroon.into()))),
1229 Action::Print('r'),
1230 Action::Print('e'),
1231 Action::Print('d'),
1232 Action::Esc(Esc::Code(EscCode::AsciiCharacterSetG0)),
1234 Action::CSI(CSI::Sgr(Sgr::Reset)),
1235 Action::CSI(CSI::Sgr(Sgr::Foreground(AnsiColor::Maroon.into()))),
1236 Action::Print('2'),
1237 ]
1238 );
1239
1240 assert_eq!(
1241 out.renderer.current_attr,
1242 CellAttributes::default()
1243 .set_foreground(AnsiColor::Maroon)
1244 .clone()
1245 );
1246 }
1247
1248 #[test]
1249 fn truecolor() {
1250 let mut out = FakeTerm::new(xterm_terminfo());
1251 out.render(&[
1252 Change::Attribute(AttributeChange::Foreground(
1253 ColorSpec::TrueColor((255, 128, 64).into()).into(),
1254 )),
1255 Change::Text("A".into()),
1256 ])
1257 .unwrap();
1258
1259 let result = out.parse();
1260 assert_eq!(
1261 result,
1262 vec![
1263 Action::CSI(CSI::Sgr(Sgr::Foreground(
1264 ColorSpec::TrueColor((255, 128, 64).into()).into(),
1265 ))),
1266 Action::Print('A'),
1267 ]
1268 );
1269 }
1270
1271 #[test]
1272 fn truecolor_no_terminfo() {
1273 let mut out = FakeTerm::new(no_terminfo_all_enabled());
1274 out.render(&[
1275 Change::Attribute(AttributeChange::Foreground(
1276 ColorSpec::TrueColor((255, 128, 64).into()).into(),
1277 )),
1278 Change::Text("A".into()),
1279 ])
1280 .unwrap();
1281
1282 let result = out.parse();
1283 assert_eq!(
1284 result,
1285 vec![
1286 Action::CSI(CSI::Sgr(Sgr::Foreground(
1287 ColorSpec::TrueColor((255, 128, 64).into()).into(),
1288 ))),
1289 Action::Print('A'),
1290 ]
1291 );
1292 }
1293}