Files
ai-app/app/androidApp/src/main/kotlin/com/example/aiapp/Theme.kt
T
irisandClaude Opus 5 edc39c7371 Thin the app's comments
The same pass the server had, on the Kotlin side: comments restating what
the code says are gone, and the ones recording a measurement, a constraint
or an incident are kept but cut to a few lines each. 6540 comment lines to
5674, and 920 lines off the app.

Two doc comments had drifted onto the item above the one they describe --
`contextAfter`'s onto `sessionWorking` in Events.kt, and `UsageMonitor`'s
equivalent on the server was fixed in the previous commit. Each is back on
its own item, which is the only non-comment line this diff moves.

The comments are reflowed to the column limit at their own indentation:
several were written wide, and ktfmt re-wrapped them into lines holding a
single orphan word. `/tmp` script, not kept -- ktfmt is idempotent over the
result, which is the check.

Left alone deliberately: this codebase's remaining comment density is high
because the comments carry things the code cannot say -- what a null means,
what a number was measured against, which bug a guard exists for. Of the
238 one-line doc comments in the app, five were pure restatement of the
name and were removed; the rest each say something the signature does not.

ktfmtFormat, compileDebugKotlin, lintDebug and testDebugUnitTest pass;
cargo test (127), clippy --all-targets and fmt still clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:20:16 -04:00

337 lines
13 KiB
Kotlin

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)