Skip to main content

max / makeover-layout

0.44.0: say what a run of code is, and which side of a change a line is on Two members, both from quasicoherent decision 19d7602d (option d), and both `#[non_exhaustive]` from this commit. `Syntax` is what a run of source code is, once something has classified it: the seven classes MNW's source browser already fixed across all 31 themes, plus `Plain` for a run nothing claimed. The description carries the classification and never the source. An app that browses source already has a lexer, a renderer does not and should not grow one, and three renderers each growing their own would disagree about the same file. `docengine`'s `Emphasis` crosses the same seam the same way. It is a `name()` rather than an `Intent`, and that is the interesting half: an intent resolves against a makeover theme token and there are none for syntax colours, because a highlighting palette is deliberately held fixed while everything around it changes. So the name is what a renderer maps, and a renderer with no colours to spend maps them all onto one face and is not wrong. `Change` is which side of a diff a line is on. Three members, because context is a state and not an absence: said as an `Option` with `None` for context, a renderer could not tell an unchanged line from a line nobody marked. It does take an `Intent`, reusing the status axis rather than minting a palette, since a diff's green and red mean what success and danger mean everywhere else. Both matches inside this crate are exhaustive rather than wildcarded, which is `State::suppresses_interaction`'s rule: `#[non_exhaustive]` does not apply within the defining crate, so a class added here has to be given a spelling rather than inheriting one in silence.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-02 15:27 UTC
Signed with PGP, not checked
Commit: 6092bfc1975d8bf6b83ab963c7ca47ee32bb1c43
Parent: 5b85611
2 files changed, +197 insertions, -1 deletion
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.43.0"
3 + version = "0.44.0"
4 4 edition = "2024"
5 5 # One copy of this vocabulary per dependency graph, enforced by cargo rather
6 6 # than by remembering. Two versions of a description layer in one build means
M src/lib.rs +196
@@ -5002,8 +5002,204 @@
5002 5002 }
5003 5003 }
5004 5004
5005 + /// What a run of source code is, once something has classified it.
5006 + ///
5007 + /// The description carries the classification and never the source, which is
5008 + /// the whole of decision `19d7602d` (2026-09-02, option d). An app that browses
5009 + /// source already has a lexer; a renderer does not and should not grow one, and
5010 + /// three renderers each growing their own would disagree about the same file.
5011 + ///
5012 + /// # Why the app classifies and the renderer colours
5013 + ///
5014 + /// Measured on MNW's source browser, which is the only consumer in the tree.
5015 + /// It highlights server-side with syntect and had already reduced syntect's
5016 + /// scope space to seven colours held fixed across all 31 themes, because a
5017 + /// reader recognises a highlighting palette and re-tinting it per theme costs
5018 + /// that recognition to gain nothing. So the classification existed on the app
5019 + /// side already: the only question was whether to throw it away at the seam and
5020 + /// have each renderer redo it. This is the answer.
5021 + ///
5022 + /// The precedent is `docengine`'s `Emphasis`, which crosses the same seam the
5023 + /// same way: `quasi-tui` maps its four flags onto terminal modifiers, a webview
5024 + /// maps them onto elements, and neither parses markdown to do it.
5025 + ///
5026 + /// # The eight, and why these eight
5027 + ///
5028 + /// The seven MNW's palette fixes, plus [`Plain`](Self::Plain) for a run nothing
5029 + /// claimed. `Plain` is not an absence: a lexer that ran and found ordinary code
5030 + /// is saying something a renderer wants, and an `Option<Syntax>` would have made
5031 + /// "unclassified" and "not classified yet" one value.
5032 + ///
5033 + /// `#[non_exhaustive]` from the first commit, deliberately. A ninth class is the
5034 + /// obvious next request and it must not be a breaking release across three
5035 + /// renderers and five apps.
5036 + ///
5037 + /// # What it is not
5038 + ///
5039 + /// A token type in a grammar. These are display classes, coarse on purpose:
5040 + /// the distinctions a reader uses at a glance, not the ones a parser makes.
5041 + /// A renderer wanting more has `language` on the node beside this and may do
5042 + /// whatever it likes with it.
5043 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
5044 + #[non_exhaustive]
5045 + pub enum Syntax {
5046 + /// Ordinary code nothing else claimed.
5047 + ///
5048 + /// The default, and a real answer rather than a missing one. See the type's
5049 + /// docs for why this is not an `Option`.
5050 + #[default]
5051 + Plain,
5052 + /// A language keyword, and the storage and modifier words with it.
5053 + Keyword,
5054 + /// A string or character literal.
5055 + String,
5056 + /// A comment.
5057 + Comment,
5058 + /// A literal that is not a string: a number, a boolean, a constant name.
5059 + Constant,
5060 + /// A name being defined: a function, a type, a module.
5061 + Entity,
5062 + /// A name being used: a variable, a parameter, a field.
5063 + Variable,
5064 + /// Something the language or its library provides rather than this file.
5065 + Support,
5066 + }
5067 +
5068 + impl Syntax {
5069 + /// The stable lowercase name, for a renderer keying its own palette off it.
5070 + ///
5071 + /// Named here rather than agreed between each renderer and each host, for
5072 + /// the reason every other spelling in this crate is: that is how one
5073 + /// renderer ends up calling it `str` and the next `string`, and a theme
5074 + /// written against one stops working under the other.
5075 + ///
5076 + /// Not an [`Intent`] token, and the difference is worth stating. An intent
5077 + /// resolves against a `makeover` theme token, and there are none for syntax
5078 + /// colours: a highlighting palette is deliberately outside the theme, held
5079 + /// fixed while everything around it changes. So this is a name a renderer
5080 + /// maps however it can, and a renderer with no colours to spend maps every
5081 + /// one of them onto the same face and is not wrong.
5082 + #[must_use]
5083 + pub const fn name(self) -> &'static str {
5084 + match self {
5085 + Self::Plain => "plain",
5086 + Self::Keyword => "keyword",
5087 + Self::String => "string",
5088 + Self::Comment => "comment",
5089 + Self::Constant => "constant",
5090 + Self::Entity => "entity",
5091 + Self::Variable => "variable",
5092 + Self::Support => "support",
5093 + // Exhaustive rather than wildcarded, which is `State`'s rule one
5094 + // type up: within this crate `#[non_exhaustive]` does not apply, so
5095 + // a class added here has to be given a spelling rather than
5096 + // inheriting "plain" in silence. Consumers outside the crate take
5097 + // the wildcard, and for them "plain" is the right degradation.
5098 + }
5099 + }
5100 + }
5101 +
5102 + /// What one line of a diff is: added, removed, or neither.
5103 + ///
5104 + /// The other half of decision `19d7602d`. A diff is a table of lines and the
5105 + /// only thing the vocabulary was missing was a way for a line to say which side
5106 + /// of the change it is on, so this rides on [`Cells`] rather than arriving as a
5107 + /// `Node::Diff` carrying git's data model.
5108 + ///
5109 + /// # Why three and not two
5110 + ///
5111 + /// [`Context`](Self::Context) is a line that did not change, and it is most of
5112 + /// a diff. Said as `Option<Change>` with `None` for context, a renderer could
5113 + /// not tell an unchanged line from a line nobody marked, which is the same
5114 + /// argument [`Syntax::Plain`] makes one type up.
5115 + ///
5116 + /// `#[non_exhaustive]` for [`Syntax`]'s reason. A fourth kind -- a moved line, a
5117 + /// conflict side -- is a plausible request and must not be a breaking release.
5118 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
5119 + #[non_exhaustive]
5120 + pub enum Change {
5121 + /// The line is in both sides and did not change.
5122 + #[default]
5123 + Context,
5124 + /// The line is only in the new side.
5125 + Added,
5126 + /// The line is only in the old side.
5127 + Removed,
5128 + }
5129 +
5130 + impl Intent for Change {
5131 + fn token(self) -> &'static str {
5132 + match self {
5133 + // The status axis, reused rather than given a palette of its own.
5134 + // A diff's green and red mean exactly what success and danger mean
5135 + // everywhere else in the system, and an app that wants them held
5136 + // fixed across themes says so in its own sheet, which is what MNW's
5137 + // `--diff-add` / `--diff-del` pair already does.
5138 + Self::Added => "success",
5139 + Self::Removed => "danger",
5140 + // Context is the bulk of a diff and is ordinary content. Muting it
5141 + // would be the axis deciding that unchanged lines are less worth
5142 + // reading, which is a renderer's call about a particular view.
5143 + //
5144 + // Exhaustive for `Syntax::name`'s reason: a fourth kind must answer
5145 + // this rather than inherit an answer.
5146 + Self::Context => "content",
5147 + }
5148 + }
5149 + }
5150 +
5005 5151 #[cfg(test)]
5006 5152 mod tests {
5153 + use super::{Change, Intent as _, Syntax};
5154 +
5155 + /// A run nothing claimed is a real answer rather than a missing one, which
5156 + /// is why this is not an `Option<Syntax>`: a lexer that ran and found
5157 + /// ordinary code has said something.
5158 + #[test]
5159 + fn an_unclassified_run_is_plain_and_plain_is_the_default() {
5160 + assert_eq!(Syntax::default(), Syntax::Plain);
5161 + assert_eq!(Syntax::Plain.name(), "plain");
5162 + }
5163 +
5164 + /// Every class has a stable lowercase name, and no two share one. The
5165 + /// spelling is named here rather than agreed per renderer, which is what
5166 + /// stops one calling it `str` and the next `string`.
5167 + #[test]
5168 + fn every_syntax_class_has_its_own_spelling() {
5169 + let all = [
5170 + Syntax::Plain,
5171 + Syntax::Keyword,
5172 + Syntax::String,
5173 + Syntax::Comment,
5174 + Syntax::Constant,
5175 + Syntax::Entity,
5176 + Syntax::Variable,
5177 + Syntax::Support,
5178 + ];
5179 + let mut seen = std::collections::BTreeSet::new();
5180 + for class in all {
5181 + assert!(!class.name().is_empty(), "{class:?} has no spelling");
5182 + assert!(seen.insert(class.name()), "{class:?} shares a spelling");
5183 + }
5184 + assert_eq!(seen.len(), all.len());
5185 + }
5186 +
5187 + /// Context is a state and not an absence, so an unchanged line is
5188 + /// distinguishable from a line nobody marked. `Syntax::Plain`'s argument.
5189 + #[test]
5190 + fn an_unmarked_diff_line_is_context() {
5191 + assert_eq!(Change::default(), Change::Context);
5192 + assert_eq!(Change::Context.token(), "content");
5193 + }
5194 +
5195 + /// The two sides reuse the status axis rather than minting a palette. An
5196 + /// app holding them fixed across themes says so in its own sheet.
5197 + #[test]
5198 + fn the_two_sides_of_a_change_take_the_status_intents() {
5199 + assert_eq!(Change::Added.token(), "success");
5200 + assert_eq!(Change::Removed.token(), "danger");
5201 + }
5202 +
5007 5203 /// The concept is named here and its magnitude is not, which is
5008 5204 /// `Awaiting`'s split and is why a renderer can disagree with another
5009 5205 /// about what a level is worth without either of them being wrong.