//! The terminal renderer for [`makeover_layout`]. //! //! //! //! Named for the target and not for ratatui, the same way //! `makeover-immediate` is named for the mode and not for egui. //! //! # What a terminal actually costs you //! //! Not colour. That was the original assumption here and it is wrong on any //! terminal built this decade. Measured across the 31 shipped themes //! (`makeover`'s `well_fidelity` example): //! //! | | ANSI-16 | ANSI-256 | truecolor | //! |---|---|---|---| //! | a well collapses onto its face | 18/31 | 4/31 | 2/31 | //! | at least one bevel edge vanishes into its face | 31/31 | 4/31 | 0 | //! //! The threshold is 256, not 24-bit, and the two failures that survive at //! truecolor are not terminal failures at all: they are the themes whose //! raised surface is already white, so the lightening clamps and the well //! lands exactly on its face. Those render identically in a browser. //! `makeover`'s own `well_is_distinct_from_its_face` test already names them. //! //! **What a terminal costs is geometry, and no amount of colour fixes it.** //! An edge occupies a whole cell on each side. A cell is roughly 8x17 pixels, //! so a one-pixel bevel becomes something an order of magnitude heavier, which //! is why [`frame`] hands back a shrunk [`Rect`] instead of pretending the //! region survived intact. There is nowhere to put a corner radius, so //! `radius_control` and `radius_container` mean the same thing here. A fill //! can only begin and end on a cell boundary. //! //! That is the constraint worth designing against. It does not improve, it is //! not detectable, and it applies equally to the best terminal ever written. //! //! What it does not mean is that the shape inside the cell stops mattering. //! Half of a cell is still addressable, and a bevel drawn in half-blocks reads //! as a lit edge where the same bevel in box-drawing reads as a line: `─` and //! `│` are one stroke through the middle, identical on all four sides, saying //! nothing about where the light is. Half-blocks also make the two corners //! where light meets shadow expressible, since a glyph that fills half a cell //! leaves the other half to the second tone. //! //! # Where fidelity does matter //! //! At [`Fidelity::Ansi16`] the depth vocabulary collapses outright: a well //! cannot be filled distinctly on most themes *and* a bevel loses an edge on //! every one of them, so a raised card and a well both read as a single-tone //! box. Colour cannot carry the distinction, so [`frame`] carries it with the //! glyphs instead. //! //! Above that, colour carries it and the glyph fallback never fires. //! //! [`Palette::shows`] is worth reading correctly in light of the numbers: it //! is **not** a low-colour workaround. It is a correctness check that a fill //! will be visible against what is behind it, and at truecolor it fires on //! exactly the two clamping themes, which is precisely when it should. //! //! # The correction this renderer forced //! //! [`makeover_layout::Fill`] briefly carried a `fallback` method, returning //! `Page` for `Well` so a consumer without `surface-well` had something to //! use. That is an answer for a renderer that can always paint a colour. Here //! it is actively wrong: page *is* the surface a well is usually cut into, so //! falling back to it produces the exact invisibility the fallback was meant //! to avoid. //! //! Substituting one intent for another is renderer policy, not description. //! The fallback moved out of the description and into //! `makeover-immediate`, where it belongs, which is the first thing a second //! renderer was built to find. #![forbid(unsafe_code)] use makeover_layout::{Bevel, Depth, Edge, Fill}; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Color; /// The description this crate renders, re-exported. /// /// Every entry point here takes a type from it, so a consumer would otherwise /// have to depend on the description separately and keep two version /// requirements in step to name the argument it is already being handed. pub use makeover_layout; /// A loaded makeover theme, resolved to the colours ratatui draws with. /// /// Behind the `theme` feature: it is the only thing here that needs `makeover` /// itself, and that crate embeds the shipped theme files. A consumer that wants /// [`frame`] and nothing else should not carry them. #[cfg(feature = "theme")] pub mod theme; #[cfg(feature = "theme")] pub use theme::{Mode, Quantize, Theme, ThemeError}; /// How many colours the terminal can actually show. /// /// Only [`Fidelity::Ansi16`] changes what this crate draws. Above it, colour /// separates a raised surface from a well on every shipped theme, and the /// glyph fallback below never fires. Recorded rather than inferred, because a /// caller that quantised its palette knows the answer and this crate cannot /// recover it from the colours afterwards. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Fidelity { /// Sixteen colours. Depth cannot be carried by colour: a well collapses /// onto its face on 18 of 31 themes and a bevel loses an edge on all 31. Ansi16, /// The 6x6x6 cube and the grey ramp. Enough on 27 of 31 themes. Ansi256, /// 24-bit. The only failures left belong to the theme, not the terminal. #[default] TrueColor, } impl Fidelity { /// Read the terminal's own claim, from `COLORTERM` then `TERM`. /// /// Deliberately credulous, and the fall-through is where that is decided. /// An unrecognised `TERM` is assumed capable, because the two wrong answers /// do not cost the same: guessing [`TrueColor`](Self::TrueColor) on a /// limited terminal costs some fidelity, and guessing /// [`Ansi16`](Self::Ansi16) on a capable one throws away colour the user /// paid for — and, for a caller that quantises its palette off this answer, /// throws away the whole theme. `COLORTERM` is routinely stripped by ssh /// and by multiplexers, so an unrecognised name is the common case rather /// than the exotic one: `foot`, `xterm` and `screen` all land here. /// /// So sixteen colours is reached by naming the terminals that really have /// them. The list is short and it does not grow: these are the fixed /// consoles, and `TERM=linux` is the case this exists for — the Linux /// virtual console, which is what an installer and a machine with no /// desktop draw on. #[must_use] pub fn detect() -> Self { Self::from_env( &std::env::var("COLORTERM").unwrap_or_default(), &std::env::var("TERM").unwrap_or_default(), ) } /// [`detect`](Self::detect) with the environment passed in, so the decision /// can be tested without mutating a process-wide variable from a parallel /// test. #[must_use] pub fn from_env(colorterm: &str, term: &str) -> Self { if colorterm.contains("truecolor") || colorterm.contains("24bit") { return Self::TrueColor; } match term { "linux" | "vt100" | "vt220" | "ansi" | "dumb" => Self::Ansi16, _ if term.contains("256color") || term.contains("direct") => Self::Ansi256, _ => Self::TrueColor, } } /// Whether colour alone can tell a raised surface from a well here. #[must_use] pub const fn separates_depth(self) -> bool { !matches!(self, Self::Ansi16) } } /// The resolved colours this renderer needs. /// /// Supply them already quantised to whatever the terminal can show. That is /// what makes [`Palette::shows`] a plain inequality rather than a colour-space /// calculation: by the time a colour reaches here, the question of what the /// terminal will actually paint has been answered. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Palette { /// `surface-page`. pub page: Color, /// `surface-raised`. pub raised: Color, /// `surface-overlay`. pub overlay: Color, /// `surface-well`, absent on makeover before 2.3.0. pub well: Option, /// `bevel-light`. pub bevel_light: Color, /// `bevel-dark`. pub bevel_dark: Color, /// What the terminal can show. Defaults to [`Fidelity::TrueColor`]. pub fidelity: Fidelity, } impl Palette { /// Resolve a surface intent, or `None` where this renderer has no colour /// for it. /// /// No substitution happens here. A missing intent stays missing, and /// [`frame`] answers it with structure instead of with a different colour. /// That rule is what lets the wildcard below be a real answer rather than /// a hole: [`Fill`] is `#[non_exhaustive]` from `makeover-layout` 0.4.0 /// onward, so the description can name a surface this renderer has not /// learned to paint, and saying so is better than failing to build. #[must_use] pub const fn fill(&self, fill: Fill) -> Option { match fill { Fill::Page => Some(self.page), Fill::Raised => Some(self.raised), Fill::Overlay => Some(self.overlay), Fill::Well => self.well, // Includes Fill::Sunken, which this renderer has no tone for: a // terminal cell has one background, so a surface set back by // colour alone is not a thing it can say. The chosen tab is drawn // forward instead. _ => None, } } /// Resolve a bevel edge intent. #[must_use] pub const fn edge(&self, edge: Edge) -> Color { match edge { Edge::Light => self.bevel_light, Edge::Dark => self.bevel_dark, } } /// Whether painting `fill` over `behind` would show anything. /// /// The whole of the terminal's problem in one predicate. On a truecolor /// terminal this is almost always true; in sixteen colours it is false /// often enough that a design relying on fills is a design that vanishes. #[must_use] pub fn shows(fill: Color, behind: Color) -> bool { fill != behind } /// Whether this palette can express a bevel as two distinct edges. /// /// Measured, this is the wrong thing to worry about: the two edge colours /// never quantise onto each other, at any depth, on any shipped theme. /// What does happen is an edge vanishing into the *face* it is drawn on, /// on every theme at sixteen colours. Kept because a hand-built palette /// can still collide, and cheap to ask. #[must_use] pub fn two_tone(&self) -> bool { self.bevel_light != self.bevel_dark } /// Whether depth has to be carried by glyphs rather than by colour. /// /// True when the terminal cannot separate the two surfaces, which is the /// sixteen-colour case and nothing else. #[must_use] pub const fn needs_glyph_depth(&self) -> bool { !self.fidelity.separates_depth() } } /// The characters a frame's edges and corners are drawn with. /// /// Per side rather than per axis, because the set that reads best as a bevel /// does not use the same glyph on opposite sides: a half-block edge is only /// half a cell, and which half it occupies is what says where the edge is. /// Box-drawing sets fill `top`/`bottom` and `left`/`right` with the same /// character and lose nothing by it. /// /// Three sets. [`BEVEL`] is what a terminal that can show two tones gets. The /// other two exist because at sixteen colours the glyphs are the only thing /// left to carry depth: a well cannot be filled distinctly and a bevel loses /// an edge, so a raised card and a well would otherwise be the same /// single-tone box. A doubled line reads as standing off the page and a light /// one as cut into it, which is the same claim the fill and the bevel make in /// colour. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct GlyphSet { pub(crate) top: &'static str, pub(crate) bottom: &'static str, pub(crate) left: &'static str, pub(crate) right: &'static str, pub(crate) top_left: &'static str, pub(crate) top_right: &'static str, pub(crate) bottom_left: &'static str, pub(crate) bottom_right: &'static str, /// Whether the two corners where light meets shadow carry both tones in /// one cell, foreground over background. /// /// Only a half-cell glyph can: it already divides the cell, so the split /// costs nothing and the corner reads as a transition rather than as one /// edge overrunning the other. A box-drawing corner is a single stroke /// with no such division, so those sets say `false` and both shared /// corners go to dark — see [`paint_bevel_with`] for why that particular /// fallback and not the other one. pub(crate) split_corners: bool, } /// Half-blocks, which is what a bevel actually wants. /// /// A cell is roughly 8x17 device pixels, so a half-block along the top and a /// half-cell column down the side are about the same number of pixels and the /// edge reads as even thickness. Box-drawing cannot do that: `─` and `│` are /// both a thin stroke through the middle of the cell, identical on all four /// sides, which draws a *line* rather than a lit edge and gives up the light /// model that makes a bevel legible. /// /// Adopted from `alloy_tui`, which reached this independently and got there /// first (2026-07-26, two days before this crate existed). pub(crate) const BEVEL: GlyphSet = GlyphSet { top: "▀", bottom: "▄", left: "▌", right: "▐", top_left: "▛", // The two shared corners are the split ones: an upper half continues the // lit top edge while the lower half starts the shaded right edge, and the // mirror of that at bottom left. top_right: "▀", bottom_left: "▄", bottom_right: "▟", split_corners: true, }; pub(crate) const LIGHT: GlyphSet = GlyphSet { top: "─", bottom: "─", left: "│", right: "│", top_left: "┌", top_right: "┐", bottom_left: "└", bottom_right: "┘", split_corners: false, }; pub(crate) const DOUBLE: GlyphSet = GlyphSet { top: "═", bottom: "═", left: "║", right: "║", top_left: "╔", top_right: "╗", bottom_left: "╚", bottom_right: "╝", split_corners: false, }; /// Paint a two-tone edge around the outside of `area`. /// /// Light takes the top and left, dark the bottom and right. What happens at /// the two corners where they meet depends on what the terminal can show. /// Above sixteen colours the edge is drawn in half-blocks and those corners /// carry both tones, one per half-cell. At sixteen it is box-drawing, whose /// single stroke has no half to give, so both shared corners go to dark. /// /// Costs a cell on each side, which a pixel renderer's bevel does not. Use the /// [`Rect`] returned by [`frame`] rather than assuming the area is intact. pub fn paint_bevel(buf: &mut Buffer, area: Rect, bevel: Bevel, palette: &Palette) { paint_bevel_with(buf, area, bevel, palette, set_for(palette, None)); } /// Which glyphs to draw with, given what the terminal can show. /// /// Above sixteen colours the two tones are available and [`BEVEL`] renders /// them as light. At sixteen the tones collapse, so the box-drawing sets carry /// the distinction in weight instead, and `depth` picks which: a doubled frame /// for a raised card and a light one for everything else. `None` means the /// caller is drawing a bevel with no depth behind it, which is never the /// doubled case. fn set_for(palette: &Palette, depth: Option) -> GlyphSet { if !palette.needs_glyph_depth() { return BEVEL; } match depth { Some(Depth::Raised) => DOUBLE, _ => LIGHT, } } fn paint_bevel_with(buf: &mut Buffer, area: Rect, bevel: Bevel, palette: &Palette, set: GlyphSet) { if area.width < 2 || area.height < 2 { return; } let (top_left, bottom_right) = bevel.edges(); let light = palette.edge(top_left); let dark = palette.edge(bottom_right); let (x0, y0) = (area.x, area.y); let (x1, y1) = (area.right() - 1, area.bottom() - 1); // Light first: top edge and left edge, corners included. for x in x0..=x1 { buf[(x, y0)].set_symbol(set.top).set_fg(light); } for y in y0..=y1 { buf[(x0, y)].set_symbol(set.left).set_fg(light); } // Dark second, so on a set without split corners the two shared ones land // on it by draw order alone. for x in x0..=x1 { buf[(x, y1)].set_symbol(set.bottom).set_fg(dark); } for y in y0..=y1 { buf[(x1, y)].set_symbol(set.right).set_fg(dark); } buf[(x0, y0)].set_symbol(set.top_left).set_fg(light); buf[(x1, y1)].set_symbol(set.bottom_right).set_fg(dark); if set.split_corners { // Where light meets shadow, both tones share the cell: the half the // glyph fills is the foreground and the half it leaves is the // background, so the corner is a transition rather than one edge // overrunning the other. buf[(x1, y0)] .set_symbol(set.top_right) .set_fg(light) .set_bg(dark); buf[(x0, y1)] .set_symbol(set.bottom_left) .set_fg(dark) .set_bg(light); } else { // Both shared corners to dark. Not arbitrary: it is the same rule // `makeover-immediate` produces by drawing its dark polyline second, // so a control does not change which corner is lit when it moves // between a terminal and a window. A single-stroke corner has no half // to give the other tone, so this is the only rule available to these // sets anyway. buf[(x1, y0)].set_symbol(set.top_right).set_fg(dark); buf[(x0, y1)].set_symbol(set.bottom_left).set_fg(dark); } } /// Draw a region at a given [`Depth`] and return the area left for content. /// /// The fill is painted only when it would be visible against what is already /// in the buffer. Everything else is the edge, which is why a well still reads /// as a well on a terminal that cannot colour one. pub fn frame(buf: &mut Buffer, area: Rect, depth: Depth, palette: &Palette) -> Rect { if area.is_empty() { return area; } let behind = buf[(area.x, area.y)].bg; if let Some(color) = depth.fill().and_then(|f| palette.fill(f)) && Palette::shows(color, behind) { for y in area.top()..area.bottom() { for x in area.left()..area.right() { buf[(x, y)].set_bg(color); } } } match depth.bevel() { Some(bevel) if area.width >= 2 && area.height >= 2 => { // Colour separates raised from well wherever it can. Where it // cannot, the glyphs do, and only then: a doubled frame on every // terminal would be shouting. let set = set_for(palette, Some(depth)); paint_bevel_with(buf, area, bevel, palette, set); Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2) } _ => area, } } #[cfg(test)] mod tests { use super::*; fn palette(well: Option) -> Palette { Palette { page: Color::Indexed(7), raised: Color::Indexed(15), overlay: Color::Indexed(8), well, bevel_light: Color::Indexed(15), bevel_dark: Color::Indexed(0), fidelity: Fidelity::TrueColor, } } fn buffer() -> Buffer { Buffer::empty(Rect::new(0, 0, 6, 4)) } #[test] fn a_well_that_cannot_be_coloured_is_still_drawn() { // The 18-of-31 case: no surface-well token at all. let p = palette(None); let mut buf = buffer(); frame(&mut buf, Rect::new(0, 0, 6, 4), Depth::Well, &p); // No fill was available, but the region still reads as recessed. assert_eq!(buf[(0, 0)].symbol(), BEVEL.top_left); assert_eq!(buf[(0, 0)].bg, Color::Reset); } #[test] fn a_fill_that_matches_its_surroundings_is_not_painted() { let p = palette(Some(Color::Indexed(7))); let mut buf = buffer(); // Everything behind is already page-coloured, and the well quantised // onto it. Painting it would be a no-op that hides the real problem. for y in 0..4 { for x in 0..6 { buf[(x, y)].set_bg(Color::Indexed(7)); } } frame(&mut buf, Rect::new(0, 0, 6, 4), Depth::Well, &p); assert!(!Palette::shows(Color::Indexed(7), Color::Indexed(7))); // The edge is what carries the meaning here. assert_eq!(buf[(5, 3)].symbol(), BEVEL.bottom_right); } #[test] fn a_visible_fill_is_painted() { let p = palette(Some(Color::Indexed(4))); let mut buf = buffer(); frame(&mut buf, Rect::new(0, 0, 6, 4), Depth::Well, &p); assert_eq!(buf[(2, 2)].bg, Color::Indexed(4)); } #[test] fn the_light_falls_from_the_top_left() { let p = palette(None); let mut buf = buffer(); paint_bevel(&mut buf, Rect::new(0, 0, 6, 4), Bevel::Raised, &p); assert_eq!(buf[(0, 0)].fg, p.bevel_light); // top-left assert_eq!(buf[(3, 0)].fg, p.bevel_light); // top edge assert_eq!(buf[(0, 2)].fg, p.bevel_light); // left edge assert_eq!(buf[(5, 3)].fg, p.bevel_dark); // bottom-right assert_eq!(buf[(3, 3)].fg, p.bevel_dark); // bottom edge assert_eq!(buf[(5, 2)].fg, p.bevel_dark); // right edge } // Half-cell glyphs divide the cell already, so the corner where light // meets shadow can hold both rather than picking one. #[test] fn the_shared_corners_carry_both_tones_when_the_glyph_can_split() { let p = palette(None); let mut buf = buffer(); paint_bevel(&mut buf, Rect::new(0, 0, 6, 4), Bevel::Raised, &p); let top_right = &buf[(5, 0)]; assert_eq!(top_right.fg, p.bevel_light); assert_eq!(top_right.bg, p.bevel_dark); let bottom_left = &buf[(0, 3)]; assert_eq!(bottom_left.fg, p.bevel_dark); assert_eq!(bottom_left.bg, p.bevel_light); } // A single-stroke corner has no half to give the second tone, so the // box-drawing sets keep the old rule: both shared corners to dark, which // is what makeover-immediate produces by drawing its dark polyline second. // Changing that would move the lit corner between a terminal and a window. #[test] fn box_drawing_corners_stay_dark_and_match_the_immediate_renderer() { let p = Palette { fidelity: Fidelity::Ansi16, ..palette(None) }; let mut buf = buffer(); paint_bevel(&mut buf, Rect::new(0, 0, 6, 4), Bevel::Raised, &p); assert_eq!(buf[(5, 0)].symbol(), LIGHT.top_right); assert_eq!(buf[(5, 0)].fg, p.bevel_dark); assert_eq!(buf[(5, 0)].bg, Color::Reset, "a stroke has no second tone"); assert_eq!(buf[(0, 3)].fg, p.bevel_dark); } // The whole outline, as a reader sees it. Asserted as glyphs because the // shape is the point: an even-weight edge on all four sides, which is what // box-drawing could not give. #[test] fn a_bevel_draws_an_even_outline_and_leaves_the_middle_alone() { let p = palette(None); let mut buf = Buffer::empty(Rect::new(0, 0, 5, 4)); paint_bevel(&mut buf, Rect::new(0, 0, 5, 4), Bevel::Raised, &p); let rows: Vec = (0..4) .map(|y| (0..5).map(|x| buf[(x, y)].symbol()).collect()) .collect(); assert_eq!(rows, vec!["▛▀▀▀▀", "▌ ▐", "▌ ▐", "▄▄▄▄▟"]); } #[test] fn pressing_swaps_the_lit_side() { let p = palette(None); let mut buf = buffer(); paint_bevel(&mut buf, Rect::new(0, 0, 6, 4), Bevel::Raised.pressed(), &p); assert_eq!(buf[(0, 0)].fg, p.bevel_dark); } #[test] fn a_sixteen_colour_terminal_can_lose_the_second_tone() { // Not a failure: one box is still a boundary. The palette says so // rather than the renderer pretending otherwise. let flat = Palette { bevel_dark: Color::Indexed(15), ..palette(None) }; assert!(!flat.two_tone()); assert!(palette(None).two_tone()); } #[test] fn an_edge_costs_a_cell_on_every_side() { let p = palette(None); let mut buf = buffer(); let inner = frame(&mut buf, Rect::new(0, 0, 6, 4), Depth::Raised, &p); assert_eq!(inner, Rect::new(1, 1, 4, 2)); // Flat takes no cells, because it draws no edge. let same = frame(&mut buf, Rect::new(0, 0, 6, 4), Depth::Flat, &p); assert_eq!(same, Rect::new(0, 0, 6, 4)); } #[test] fn sixteen_colours_carries_depth_in_the_glyphs_instead() { // Colour cannot separate raised from well here: the fill collapses on // most themes and an edge vanishes on all of them. The frame has to // say it some other way or the two become the same box. let p = Palette { fidelity: Fidelity::Ansi16, ..palette(None) }; assert!(p.needs_glyph_depth()); let mut raised = buffer(); frame(&mut raised, Rect::new(0, 0, 6, 4), Depth::Raised, &p); let mut well = buffer(); frame(&mut well, Rect::new(0, 0, 6, 4), Depth::Well, &p); assert_eq!(raised[(0, 0)].symbol(), DOUBLE.top_left); assert_eq!(well[(0, 0)].symbol(), LIGHT.top_left); assert_ne!(raised[(0, 0)].symbol(), well[(0, 0)].symbol()); } #[test] fn above_sixteen_colours_the_glyphs_stay_out_of_it() { // The doubled fallback must not fire where colour already works, or // every modern terminal gets a heavier frame it did not need. What it // gets instead is the half-block bevel. for f in [Fidelity::Ansi256, Fidelity::TrueColor] { let p = Palette { fidelity: f, ..palette(Some(Color::Indexed(4))) }; assert!(!p.needs_glyph_depth()); let mut buf = buffer(); frame(&mut buf, Rect::new(0, 0, 6, 4), Depth::Raised, &p); assert_eq!( buf[(0, 0)].symbol(), BEVEL.top_left, "{f:?} got a heavier frame" ); assert_ne!(buf[(0, 0)].symbol(), DOUBLE.top_left); } } // Raised and well are both bevels and differ only in which way they are // lit, so above sixteen colours they draw the same glyphs and the tones // carry the difference. That is exactly what stops holding at Ansi16, and // why the doubled set exists. #[test] fn colour_alone_separates_raised_from_well_where_it_can() { let p = palette(Some(Color::Indexed(4))); let mut raised = buffer(); frame(&mut raised, Rect::new(0, 0, 6, 4), Depth::Raised, &p); let mut well = buffer(); frame(&mut well, Rect::new(0, 0, 6, 4), Depth::Well, &p); assert_eq!(raised[(0, 0)].symbol(), well[(0, 0)].symbol()); assert_eq!(raised[(0, 0)].fg, p.bevel_light); assert_eq!(well[(0, 0)].fg, p.bevel_dark); } #[test] fn detection_defaults_generously_and_only_downgrades_on_evidence() { assert!(Fidelity::default().separates_depth()); assert!(Fidelity::TrueColor.separates_depth()); assert!(Fidelity::Ansi256.separates_depth()); assert!(!Fidelity::Ansi16.separates_depth()); } // Sixteen colours is reached by naming a console, never by failing to // recognise a terminal. `COLORTERM` is stripped by ssh and by every // multiplexer, so an unrecognised name carries no evidence at all, and a // caller quantising its palette off this answer would flatten a whole theme // on the strength of it. #[test] fn an_unrecognised_terminal_is_assumed_capable() { let f = Fidelity::from_env; assert_eq!(f("", "foot"), Fidelity::TrueColor); assert_eq!(f("", "xterm"), Fidelity::TrueColor); assert_eq!(f("", "screen"), Fidelity::TrueColor); assert_eq!(f("", ""), Fidelity::TrueColor); } #[test] fn a_console_that_really_has_sixteen_colours_is_named() { let f = Fidelity::from_env; assert_eq!(f("", "linux"), Fidelity::Ansi16); assert_eq!(f("", "vt100"), Fidelity::Ansi16); assert_eq!(f("", "dumb"), Fidelity::Ansi16); } #[test] fn a_terminal_naming_its_depth_is_taken_at_its_word() { let f = Fidelity::from_env; assert_eq!(f("", "xterm-256color"), Fidelity::Ansi256); assert_eq!(f("", "screen-256color"), Fidelity::Ansi256); assert_eq!(f("", "xterm-direct"), Fidelity::Ansi256); // And a claim of 24-bit beats the name, which is only ever a floor. assert_eq!(f("truecolor", "xterm-256color"), Fidelity::TrueColor); assert_eq!(f("24bit", "linux"), Fidelity::TrueColor); } #[test] fn a_region_too_small_for_an_edge_is_left_alone() { let p = palette(None); let mut buf = buffer(); let inner = frame(&mut buf, Rect::new(0, 0, 1, 1), Depth::Raised, &p); assert_eq!(inner, Rect::new(0, 0, 1, 1)); } }