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. * * The mapping that matters is the surface ladder. Mocha names its darks in order -- Crust, Mantle, * Base, Surface 0, Surface 1 -- 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. */ 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, so the same state was a slightly different colour depending which screen you looked * at. A colour that carries meaning is part of the scheme, not a value typed where it was 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. */ 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, 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 what 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. The same red as [failedColor] and [stopColor] for a * third reason: this is neither a fault nor a button, it is the mark left where something was taken * away. No two of the three 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 has no * limit to be near: it finishes. Only a bar measuring a *quota* escalates -- 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 point of colouring * by consequence is that the reader learns the step once. It reads as a difference in degree, which * is all colour can carry: the states that differ in *kind* -- a window nobody could read, a * machine that meters nothing -- are said in words elsewhere. * * [percent] is the API's own 0-100 rather than a fraction, so callers pass what the server sent * without 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 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 and on a card; a tint chosen upwards has to be picked twice * and still collides with the card it lands on. * * 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 already uses. * * Not a composable, because [highlight] runs off the drawing thread; these 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. * * 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, on Crust, the same 40% composites to a barely-there smudge, so selecting a line of * code looks like nothing happened even though it copies correctly. * * Fixed and stronger, because "this is selected" is a meaning rather than decoration. 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. */ 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. Lavender is not * one of the colours that mean something here, 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: 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. 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. */ @Composable fun actionButtonColors(fill: Color): ButtonColors = ButtonDefaults.buttonColors(containerColor = fill, contentColor = Mocha.Crust)