Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Sources/BloomCore/Presentation/CodeScheme+Builtins.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ extension CodeScheme {
public static let charcoal = builtin(
id: "charcoal", title: "Charcoal", background: .init(light: 0xFFFFFF, dark: 0x292C33)
)
public static let lagoon = themed(.lagoon)
public static let sakura = themed(.sakura)
public static let matcha = themed(.matcha)
public static let synthwave = themed(.synthwave)
public static let dune = themed(.dune)

private static func themed(_ theme: ColourTheme) -> Self {
builtin(id: theme.id, title: theme.title, background: theme.surfaces.surface)
}

/// A constant is a number as far as these schemes are concerned, and saying so is cheaper than
/// keeping two copies of one pair in step.
Expand Down
2 changes: 1 addition & 1 deletion Sources/BloomCore/Presentation/CodeScheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public struct CodeScheme: Codable, Hashable, Sendable, Identifiable {
tokens[kind] ?? foreground
}

public static let all: [Self] = [.bloom, .charcoal]
public static let all: [Self] = [.bloom, .charcoal, .lagoon, .sakura, .matcha, .synthwave, .dune]
public static func find(_ key: String?, fallback: Self = .bloom) -> Self {
all.first { $0.id == key } ?? fallback
}
Expand Down
63 changes: 63 additions & 0 deletions Sources/BloomCore/Presentation/ColourTheme+Builtins.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,67 @@ extension ColourTheme {
),
codeScheme: "charcoal", terminalScheme: "charcoal"
)

// The light reading grounds of the five below (surface, raised, sunken) sit on Bloom's own
// luminance steps and carry their colour in hue alone, because `textTertiary` and the syntax
// comment clear 4.5 to 1 on white by a few hundredths and a darker ground fails
// `ColourThemeTests`. Cyan and green hold so much luminance that a tint at Bloom's step is
// invisible, so Lagoon and Matcha take their sidebar, selection and rule a step darker than
// Bloom's. The dark members have room, which is why these themes are louder at night.

public static let lagoon = tinted(id: "lagoon", title: "Lagoon", surfaces: ThemeSurfaces(
surface: .init(light: 0xFBFFFF, dark: 0x052422),
raised: .init(light: 0xFFFFFF, dark: 0x10312F),
sunken: .init(light: 0xE6FFFE, dark: 0x062725),
sidebar: .init(light: 0xD6F4F0, dark: 0x072B29),
border: .init(light: 0x99D8D0, dark: 0x174543),
selected: .init(light: 0xBDEAE4, dark: 0x134240)
))

public static let sakura = tinted(id: "sakura", title: "Sakura", surfaces: ThemeSurfaces(
surface: .init(light: 0xFFFEFE, dark: 0x2F0C17),
raised: .init(light: 0xFFFFFF, dark: 0x4C1D2C),
sunken: .init(light: 0xFFF9FB, dark: 0x360D1B),
sidebar: .init(light: 0xFDF2F5, dark: 0x38101D),
border: .init(light: 0xF2D8E0, dark: 0x60293B),
selected: .init(light: 0xF7DFE7, dark: 0x63283C)
))

public static let matcha = tinted(id: "matcha", title: "Matcha", surfaces: ThemeSurfaces(
surface: .init(light: 0xFDFFFA, dark: 0x152405),
raised: .init(light: 0xFFFFFF, dark: 0x213110),
sunken: .init(light: 0xF0FFE0, dark: 0x172605),
sidebar: .init(light: 0xE5F3CD, dark: 0x1A2B07),
border: .init(light: 0xBCD996, dark: 0x2F4517),
selected: .init(light: 0xD5EBB6, dark: 0x2C4213)
))

public static let synthwave = tinted(id: "synthwave", title: "Synthwave", surfaces: ThemeSurfaces(
surface: .init(light: 0xFFFEFF, dark: 0x210E38),
raised: .init(light: 0xFFFFFF, dark: 0x392156),
sunken: .init(light: 0xFBF9FF, dark: 0x261040),
sidebar: .init(light: 0xF8F3FD, dark: 0x281341),
border: .init(light: 0xE5DAF3, dark: 0x4A2D6A),
selected: .init(light: 0xECE1F8, dark: 0x4B2D6D)
))

public static let dune = tinted(id: "dune", title: "Dune", surfaces: ThemeSurfaces(
surface: .init(light: 0xFFFEFD, dark: 0x201708),
raised: .init(light: 0xFFFFFF, dark: 0x372914),
sunken: .init(light: 0xFEFAF2, dark: 0x251A09),
sidebar: .init(light: 0xFCF4E7, dark: 0x281C0B),
border: .init(light: 0xECDDC6, dark: 0x49391F),
selected: .init(light: 0xF3E4CD, dark: 0x4B391E)
))

/// A theme whose code and terminal schemes share its id, so the editor and the shell stand
/// on its own ground rather than on Bloom's navy.
private static func tinted(id: String, title: String, surfaces: ThemeSurfaces) -> ColourTheme {
var surfaces = surfaces
surfaces.glassTint = surfaces.sidebar
surfaces.glassTintOpacity = 0.8
return ColourTheme(
id: id, title: title, glass: .thick, surfaces: surfaces, codeScheme: id, terminalScheme: id
)
}
}
2 changes: 1 addition & 1 deletion Sources/BloomCore/Presentation/ColourTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public struct ColourTheme: Codable, Hashable, Sendable, Identifiable, CaseIterab
public let surfaces: ThemeSurfaces

public static let defaultsKey = "colourTheme"
public static let allCases: [ColourTheme] = [.bloom, .charcoalGlass]
public static let allCases: [ColourTheme] = [.bloom, .charcoalGlass, .lagoon, .sakura, .matcha, .synthwave, .dune]

public init(
id: String, title: String, glass: ThemeGlass, surfaces: ThemeSurfaces,
Expand Down
9 changes: 9 additions & 0 deletions Sources/BloomCore/Presentation/TerminalScheme+Builtins.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ extension TerminalScheme {
public static let charcoal = builtin(
id: "charcoal", title: "Charcoal", background: ColourTheme.charcoalGlass.surfaces.sunken
)
public static let lagoon = themed(.lagoon)
public static let sakura = themed(.sakura)
public static let matcha = themed(.matcha)
public static let synthwave = themed(.synthwave)
public static let dune = themed(.dune)

private static func themed(_ theme: ColourTheme) -> Self {
builtin(id: theme.id, title: theme.title, background: theme.surfaces.sunken)
}

private static func builtin(id: String, title: String, background: PaletteInk.Pair) -> Self {
Self(id: id, title: title,
Expand Down
2 changes: 1 addition & 1 deletion Sources/BloomCore/Presentation/TerminalScheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public struct TerminalScheme: Codable, Hashable, Sendable, Identifiable {
public var light: GhosttyTheme
public var dark: GhosttyTheme

public static let all: [Self] = [.bloom, .charcoal]
public static let all: [Self] = [.bloom, .charcoal, .lagoon, .sakura, .matcha, .synthwave, .dune]
public static func find(_ key: String?, fallback: Self = .bloom) -> Self {
all.first { $0.id == key } ?? fallback
}
Expand Down
8 changes: 8 additions & 0 deletions Tests/BloomCoreTests/ColourThemeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ struct ColourThemeTests {
#expect(decoded == theme)
}

/// `find` falls back to Bloom's schemes, so a mistyped id would put the editor on Bloom's
/// navy inside another theme without anything failing.
@Test(arguments: ColourTheme.allCases)
func schemesExist(theme: ColourTheme) {
#expect(CodeScheme.all.contains { $0.id == theme.codeScheme })
#expect(TerminalScheme.all.contains { $0.id == theme.terminalScheme })
}

@Test func charcoalKeepsApprovedGlass() {
let theme = ColourTheme.charcoalGlass
#expect(theme.glass == .thick)
Expand Down
9 changes: 7 additions & 2 deletions docs/THEMES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@ The definitions are plain Swift data with Foundation `Codable` support:

| File under `Sources/BloomCore/Presentation` | Purpose |
| --- | --- |
| `ColourTheme+Builtins.swift` | Bloom and Charcoal Glass presets, including scheme references and font defaults |
| `ColourTheme+Builtins.swift` | Bloom, Charcoal Glass, Lagoon, Sakura, Matcha, Synthwave and Dune presets, including scheme references and font defaults |
| `CodeScheme+Builtins.swift` | Code backgrounds, text, gutter, selection, caret, diff tints and token colours |
| `TerminalScheme+Builtins.swift` | Complete light/dark terminal palettes using the existing `GhosttyTheme` data |
| `ThemeTypography.swift` | Optional font family, point size and line-height multiplier for code and terminal |
| `ThemeOverrides.swift` | Per theme glass and scheme changes, and the versioned settings archive |
| `TypographyOverrides.swift` | Code, terminal and conversation typography changes shared by every theme |

Charcoal Glass keeps the stable key `neutral`, so previous selections still work.
Lagoon, Sakura, Matcha, Synthwave and Dune each share their id with a code and a terminal scheme
of their own, standing on the theme's surface and sunken colours. Their light reading grounds
keep Bloom's relative luminance steps, the surface within one percent of white, because the
tertiary ink and the syntax comment only just clear 4.5 to 1 there; the colour lives in the sidebar, selection and rule in light, and in every
ground in dark. The reason is written above them in `ColourTheme+Builtins.swift`.
Colour pairs contain light and dark RGB integers. Swift definitions use `0xRRGGBB`; JSON
uses decimal integers. Glass is `off`, `thin`, `regular` or `thick`, mapped to a tint over native AppKit
sidebar material. Conversation typography retains its existing font names and size/spacing enums.
Expand Down Expand Up @@ -73,4 +78,4 @@ preset file, VS Code colour themes for code and Ghostty or `.itermcolors` files
can be adapted later without renderers knowing about external formats.

Charcoal Glass keeps its approved dark glass tint `#212938` at 40% opacity. Bloom uses its
blue sidebar colour at 80% opacity, leaving a subtler native glass effect.
blue sidebar colour at 80% opacity, leaving a subtler native glass effect. The five tinted presets do the same with their own sidebar colour.
Loading