Skip to main content

max / makeover-tui

0.4.1: Assume an unrecognised terminal is capable `detect` called itself deliberately credulous and then fell through to Ansi16 for any TERM it did not recognise. COLORTERM is stripped by ssh and by every multiplexer, so that fall-through is the common path, not the exotic one: foot, xterm and screen all landed on sixteen colours. Sixteen is now reached by naming a console. The list is short and does not grow, because these are the fixed ones. The cost of the old answer was a heavier frame while this crate was the only consumer. It stops being that: alloy_tui quantises a whole theme off this, so a wrong Ansi16 flattens every colour in it. Split from_env out so the decision is testable without mutating a process-wide variable from a parallel test.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-31 02:41 UTC
Signed with PGP, not checked
Commit: df7cbfda736f1249959668d8c783a83f3368c32f
Parent: 8d69fa8
2 files changed, +64 insertions, -12 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-tui"
3 - version = "0.4.0"
3 + version = "0.4.1"
4 4 edition = "2024"
5 5 description = "The terminal renderer for makeover-layout, on ratatui. Colour stops being the constraint above 256 entries; geometry never does, because an edge occupies a whole cell on every side."
6 6 license = "MIT"
M src/lib.rs +63 -11
@@ -106,23 +106,42 @@
106 106 impl Fidelity {
107 107 /// Read the terminal's own claim, from `COLORTERM` then `TERM`.
108 108 ///
109 - /// Deliberately credulous. A terminal that understates itself costs a
110 - /// slightly heavier frame; one that overstates itself was going to render
111 - /// wrongly regardless of what this crate assumed.
109 + /// Deliberately credulous, and the fall-through is where that is decided.
110 + /// An unrecognised `TERM` is assumed capable, because the two wrong answers
111 + /// do not cost the same: guessing [`TrueColor`](Self::TrueColor) on a
112 + /// limited terminal costs some fidelity, and guessing
113 + /// [`Ansi16`](Self::Ansi16) on a capable one throws away colour the user
114 + /// paid for — and, for a caller that quantises its palette off this answer,
115 + /// throws away the whole theme. `COLORTERM` is routinely stripped by ssh
116 + /// and by multiplexers, so an unrecognised name is the common case rather
117 + /// than the exotic one: `foot`, `xterm` and `screen` all land here.
118 + ///
119 + /// So sixteen colours is reached by naming the terminals that really have
120 + /// them. The list is short and it does not grow: these are the fixed
121 + /// consoles, and `TERM=linux` is the case this exists for — the Linux
122 + /// virtual console, which is what an installer and a machine with no
123 + /// desktop draw on.
112 124 #[must_use]
113 125 pub fn detect() -> Self {
114 - let colorterm = std::env::var("COLORTERM").unwrap_or_default();
126 + Self::from_env(
127 + &std::env::var("COLORTERM").unwrap_or_default(),
128 + &std::env::var("TERM").unwrap_or_default(),
129 + )
130 + }
131 +
132 + /// [`detect`](Self::detect) with the environment passed in, so the decision
133 + /// can be tested without mutating a process-wide variable from a parallel
134 + /// test.
135 + #[must_use]
136 + pub fn from_env(colorterm: &str, term: &str) -> Self {
115 137 if colorterm.contains("truecolor") || colorterm.contains("24bit") {
116 138 return Self::TrueColor;
117 139 }
118 - let term = std::env::var("TERM").unwrap_or_default();
119 - if term.contains("256color") || term.contains("direct") {
120 - return Self::Ansi256;
140 + match term {
141 + "linux" | "vt100" | "vt220" | "ansi" | "dumb" => Self::Ansi16,
142 + _ if term.contains("256color") || term.contains("direct") => Self::Ansi256,
143 + _ => Self::TrueColor,
121 144 }
122 - if term.is_empty() {
123 - return Self::TrueColor;
124 - }
125 - Self::Ansi16
126 145 }
127 146
128 147 /// Whether colour alone can tell a raised surface from a well here.
@@ -642,6 +661,39 @@
642 661 assert!(!Fidelity::Ansi16.separates_depth());
643 662 }
644 663
664 + // Sixteen colours is reached by naming a console, never by failing to
665 + // recognise a terminal. `COLORTERM` is stripped by ssh and by every
666 + // multiplexer, so an unrecognised name carries no evidence at all, and a
667 + // caller quantising its palette off this answer would flatten a whole theme
668 + // on the strength of it.
669 + #[test]
670 + fn an_unrecognised_terminal_is_assumed_capable() {
671 + let f = Fidelity::from_env;
672 + assert_eq!(f("", "foot"), Fidelity::TrueColor);
673 + assert_eq!(f("", "xterm"), Fidelity::TrueColor);
674 + assert_eq!(f("", "screen"), Fidelity::TrueColor);
675 + assert_eq!(f("", ""), Fidelity::TrueColor);
676 + }
677 +
678 + #[test]
679 + fn a_console_that_really_has_sixteen_colours_is_named() {
680 + let f = Fidelity::from_env;
681 + assert_eq!(f("", "linux"), Fidelity::Ansi16);
682 + assert_eq!(f("", "vt100"), Fidelity::Ansi16);
683 + assert_eq!(f("", "dumb"), Fidelity::Ansi16);
684 + }
685 +
686 + #[test]
687 + fn a_terminal_naming_its_depth_is_taken_at_its_word() {
688 + let f = Fidelity::from_env;
689 + assert_eq!(f("", "xterm-256color"), Fidelity::Ansi256);
690 + assert_eq!(f("", "screen-256color"), Fidelity::Ansi256);
691 + assert_eq!(f("", "xterm-direct"), Fidelity::Ansi256);
692 + // And a claim of 24-bit beats the name, which is only ever a floor.
693 + assert_eq!(f("truecolor", "xterm-256color"), Fidelity::TrueColor);
694 + assert_eq!(f("24bit", "linux"), Fidelity::TrueColor);
695 + }
696 +
645 697 #[test]
646 698 fn a_region_too_small_for_an_edge_is_left_alone() {
647 699 let p = palette(None);