package com.example.aiapp import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color /** * Catppuccin Mocha, as published in `catppuccin/palette`. * * Named rather than used as literals at the point of need, so the mapping below reads as the * decision it is -- "a card is Surface 0" -- and so a value can be checked against the upstream * palette without reading the layout that uses it. */ private object Mocha { val Rosewater = Color(0xFFF5E0DC) val Mauve = Color(0xFFCBA6F7) val Red = Color(0xFFF38BA8) val Peach = Color(0xFFFAB387) val Yellow = Color(0xFFF9E2AF) val Green = Color(0xFFA6E3A1) val Teal = Color(0xFF94E2D5) val Sky = Color(0xFF89DCEB) val Blue = Color(0xFF89B4FA) val Lavender = Color(0xFFB4BEFE) val Pink = Color(0xFFF5C2E7) val Text = Color(0xFFCDD6F4) val Subtext1 = Color(0xFFBAC2DE) val Subtext0 = Color(0xFFA6ADC8) val Overlay0 = Color(0xFF6C7086) val Surface2 = Color(0xFF585B70) val Surface1 = Color(0xFF45475A) val Surface0 = Color(0xFF313244) val Base = Color(0xFF1E1E2E) val Mantle = Color(0xFF181825) val Crust = Color(0xFF11111B) } /** * The app's colour scheme: Catppuccin Mocha mapped onto Material's roles. * * Copied from dev-updater rather than shared, which is a deliberate line: wg-app-link is the *link* * -- the tunnel, the pinned CA, enrollment -- and a palette is not that. The two apps looking alike * is a preference, not a contract, and the moment one wants a different accent the shared version * becomes a thing to fight rather than a thing to use. * * The mapping that matters is the surface ladder. Mocha names its darks in order -- Crust, Mantle, * Base, Surface 0, Surface 1 -- and Material asks for the same thing under different names, so the * page is Base, a component's outlined card stays Base beside it, and a project's card is Surface * 0: one visible step up, which is the whole of what the nesting has to say. * * Accents on this palette are light, so anything filled with one takes Crust for its text rather * than the near-white the roles default to. */ val AiAppColors = darkColorScheme( primary = Mocha.Mauve, onPrimary = Mocha.Crust, primaryContainer = Mocha.Surface1, onPrimaryContainer = Mocha.Mauve, secondary = Mocha.Lavender, onSecondary = Mocha.Crust, secondaryContainer = Mocha.Surface1, onSecondaryContainer = Mocha.Lavender, tertiary = Mocha.Rosewater, onTertiary = Mocha.Crust, background = Mocha.Base, onBackground = Mocha.Text, surface = Mocha.Base, onSurface = Mocha.Text, surfaceVariant = Mocha.Surface0, onSurfaceVariant = Mocha.Subtext0, surfaceContainerLowest = Mocha.Crust, surfaceContainerLow = Mocha.Mantle, surfaceContainer = Mocha.Base, surfaceContainerHigh = Mocha.Surface0, surfaceContainerHighest = Mocha.Surface0, inverseSurface = Mocha.Text, inverseOnSurface = Mocha.Base, inversePrimary = Mocha.Mauve, outline = Mocha.Overlay0, outlineVariant = Mocha.Surface2, error = Mocha.Red, onError = Mocha.Crust, errorContainer = Mocha.Surface1, onErrorContainer = Mocha.Red, scrim = Mocha.Crust, ) /** * What a session is doing, said in colour. * * Here rather than beside each screen that shows a status. These were separate literals in two * other files -- an amber, a green and a red picked off Material's defaults -- so the same state * was a slightly different colour depending which screen you looked at, and none of them belonged * to this palette at all. A colour that carries meaning is part of the scheme, not a value typed * where it happened to be needed. */ val runningColor: Color @Composable get() = Mocha.Green /** * "This went wrong on its own": a session that fell over. * * The scheme's error colour, and deliberately not "the same red as a destructive button" even * though it is the same red. They are the same red for different reasons, and a state is not an * action -- nothing here is a button. */ val failedColor: Color @Composable get() = MaterialTheme.colorScheme.error /** * About the session rather than about the task: a command, and the compaction one of them starts. * * Its own colour because it is its own kind of work. Everything else a session does is progress * through what was asked of it; this is the session acting on itself -- rewriting what it * remembers, taking a new name -- and none of it appears in the transcript as an answer to * anything. A reader who has learned that blue means "not stuck, but not replying to you either" * has learned the thing that distinguishes it from a session that has hung. */ val commandColor: Color @Composable get() = Mocha.Blue /** * A clear: the conversation taken out of what the session is given. * * Red because of what it does, not because anything went wrong -- somebody asked for this, and a * deliberate choice is not a problem to report. It is the same red as [failedColor] and [stopColor] * for a third reason, which is worth naming rather than collapsing: this is neither a fault nor a * button, it is the mark left where something was taken away. The reader never has to tell the * three apart, because no two of them can appear as the same kind of thing. */ val clearedColor: Color @Composable get() = Mocha.Red /** Waiting on a person: a question, a permission, a turn that is theirs. */ val awaitingColor: Color @Composable get() = Mocha.Peach /** Approaching a limit -- still fine, worth seeing. */ val warningColor: Color @Composable get() = Mocha.Yellow /** * The fill of a progress bar that is only reporting how far along something is. * * Blue because a bar like this reports a quantity rather than a verdict, and the scheme's primary * made it the loudest thing on a screen the reader opened to do something else. A download, or a * compaction, has no limit to be near: it finishes. Only a bar measuring a *quota* escalates, and * that one is [quotaColor]. */ val progressColor: Color @Composable get() = Mocha.Blue /** * The fill of a bar measuring how much of a quota is gone: blue, then yellow, then red. * * One function rather than the same `when` written beside each bar, because the whole point of * colouring by consequence is that the reader learns the step once -- two bars showing the same 80% * in different colours teaches nothing except that the colour cannot be trusted. It reads as a * difference in degree, which is all colour can carry: the states that differ in *kind* from this * -- a window nobody could read, a machine that meters nothing -- are said in words elsewhere, * because a reader has no way to tell those from an ordinary low number by colour alone. * * [percent] is the API's own 0-100 rather than a fraction, so callers pass what the server sent * without each converting it first and one of them getting it wrong by a factor of a hundred. */ @Composable fun quotaColor(percent: Double): Color = when { percent >= OVER_LIMIT_PERCENT -> overLimitColor percent >= WARNING_PERCENT -> warningColor else -> progressColor } /** Close enough to the limit to be worth seeing before starting something big. */ private const val WARNING_PERCENT = 75.0 /** Close enough that the next turn may be the one that is refused. */ private const val OVER_LIMIT_PERCENT = 90.0 /** * The surface verbatim text sits on: a command, a tool's output, a code block in a reply. * * The darkest value in the palette rather than a step up from the page, and that is the whole point * -- everything else on this screen is somebody's prose, and this is what a machine was handed and * what it said back, character for character. Crust sits *below* Base, so the same colour reads as * one clear step down both on the page, where a reply is drawn, and on a card, where a tool call * is; a tint chosen upwards has to be picked twice and still collides with the card it lands on. * The renderer's default code background was `surfaceVariant`, which is exactly a card's own fill * -- so a code block inside a tool call had no background at all. * * One colour for all three, so "this is verbatim" is learnable once. */ val rawSurface: Color @Composable get() = Mocha.Crust /** * Catppuccin Mocha as the highlighter's palette; see [SyntaxPalette]. * * Here with the rest of the palette rather than beside the code that highlights: the colours a * fence is drawn in are the same accents every other coloured thing in the app already uses, and * splitting them out would make code the one surface whose palette came from somewhere else. * * Not a composable, because [highlight] runs off the drawing thread; these colours never vary with * the theme. */ fun catppuccinSyntax(): SyntaxPalette = SyntaxPalette( keyword = Mocha.Mauve, string = Mocha.Green, literal = Mocha.Peach, comment = Mocha.Overlay0, metadata = Mocha.Yellow, punctuation = Mocha.Subtext0, mark = Mocha.Sky, ) /** * The sixteen terminal colours, for what a Bash tool call printed; see [AnsiPalette]. * * Catppuccin publishes its own ANSI mapping and this is it, rather than the eight accents picked by * eye: a program printing in "colour 4" means blue, and which blue is a decision the palette has * already made for every other blue on the screen. * * Mocha's bright half is the same accents as its normal half -- only the two greys differ -- which * is upstream's choice and not an omission here. A program that uses bright red to mean something * other than red is relying on a distinction its own terminal may not draw either. * * The background is [rawSurface] because that is what a tool's output is drawn on, and reverse * video needs to know what it is reversing against. */ fun ansiPalette(): AnsiPalette = AnsiPalette( colours = listOf( Mocha.Surface1, Mocha.Red, Mocha.Green, Mocha.Yellow, Mocha.Blue, Mocha.Pink, Mocha.Teal, Mocha.Subtext1, Mocha.Surface2, Mocha.Red, Mocha.Green, Mocha.Yellow, Mocha.Blue, Mocha.Pink, Mocha.Teal, Mocha.Subtext0, ), foreground = Mocha.Text, background = Mocha.Crust, ) /** * What a selection looks like, stated rather than left to Material's default. * * The default is `primary` at 40% alpha, which is a tint of whatever is behind it -- and this app * draws text on surfaces two full steps apart. Over a reply, on Base, that reads clearly. Over a * code block or a tool's output, on Crust, the same 40% composites to a barely-there smudge, so * selecting a line of code looks like nothing happened even though the selection is there and * copies correctly. * * Fixed and stronger, because "this is selected" is a meaning rather than decoration: a colour that * means something must carry its own contrast instead of borrowing it from the surface it happens * to land on. Raised only as far as it takes to read on the darkest of them -- past this the fill * starts competing with the syntax colours it sits behind, which are the thing being read. */ val AiAppSelectionColors = TextSelectionColors( handleColor = Mocha.Mauve, backgroundColor = Mocha.Mauve.copy(alpha = 0.55f), ) /** * A link. Blue is what a link is on every Catppuccin surface, and the one colour to leave alone. */ val linkColor: Color @Composable get() = Mocha.Blue /** * A list's markers: the bullets and numbers down its left edge. * * The scheme's secondary accent rather than the text colour, because a marker is structure rather * than words: coloured, the items of a list can be counted without reading them, and a nested list * reads as a shape before it reads as text. Lavender is not one of the colours that mean something * here -- green, red, peach and yellow are states and actions -- and it is the same at every depth, * since depth is said by the glyph and the indent; a colour per depth would make a difference in * degree look like one in kind. */ val listMarkerColor: Color @Composable get() = Mocha.Lavender /** Past a limit. The scheme's error colour, for the reason [failedColor] gives. */ val overLimitColor: Color @Composable get() = MaterialTheme.colorScheme.error /** * The composer's buttons, coloured by what pressing one does rather than by where it sits. * * Green makes something happen now, blue makes it happen later, orange takes back what is in * flight, red ends the process. The near-collisions with the states above are deliberate and worth * naming rather than collapsing: [runningColor] is green because a session is working, * [failedColor] is red because one fell over, [awaitingColor] is the same orange because a session * is waiting on somebody -- those are *states*, and these are *actions*. A reader never has to tell * them apart, because nothing here is a state and nothing there is pressable. */ val sendColor: Color @Composable get() = Mocha.Green /** Sending while a turn runs: the message waits rather than starting one. See [sendColor]. */ val queueColor: Color @Composable get() = Mocha.Blue /** * Interrupting the running turn: the work stops and the session stays. * * Orange rather than red because of how much it takes: only what is in flight. The process is still * there holding the conversation, and the next message starts a turn as though nothing had * happened. Red is spent on [stopColor], which is the same button in the same place when what it * would end is the session's process. */ val pauseColor: Color @Composable get() = Mocha.Peach /** Ending the session's process -- the one button here that takes something away. */ val stopColor: Color @Composable get() = Mocha.Red /** * Starting the process again, on the conversation it left. * * The same green as [sendColor] on purpose: both mean "this happens now", and they are never the * same button -- the process button only offers to start when there is nothing running to stop. */ val startColor: Color @Composable get() = Mocha.Green /** * A filled button in one of the action colours above. * * The content colour is stated here beside the fill rather than inherited. A semantic colour has to * carry its own contrast: these fills are fixed whatever the surface under them does, so the theme * will not change to rescue a foreground that stops being readable on one of them. Crust is what * every accent on this palette takes, which is the same reason `onPrimary` is Crust above. */ @Composable fun actionButtonColors(fill: Color): ButtonColors = ButtonDefaults.buttonColors(containerColor = fill, contentColor = Mocha.Crust)