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>
337 lines
13 KiB
Kotlin
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)
|