Two things, because the second could not be built on the first. The schema stamp was part of the same execute_batch as the CREATE TABLE IF NOT EXISTS statements, and it wrote unconditionally. Opening an older file therefore added no columns but flipped the version forward anyway; opening a *newer* file stamped it back down and then wrote rows the newer build could not read. Both produced a database whose recorded version was a lie, and every future schema change would have inherited it. Now the version is read before anything is applied, migrations are ordered and forward-only inside one transaction, a newer file is refused with SchemaTooNew rather than downgraded, and a supported version raised without a step to reach it fails at the first open instead of claiming success. Tested by stamping a file at version 99 and asserting both the refusal and that the stamp is left untouched. lumbridge-settings resolves compiled default -> settings.toml -> environment. The environment sits above the file deliberately: decision 0016 calls LUMBRIDGE_CLAUDE_OAUTH=0 "one switch off", and a switch a config file can silently re-enable is not a switch. A pinned value renders disabled and names the variable, rather than accepting an edit that would do nothing. Every field carries a WriteAuthority. Routing all writes through Configure is the obvious design and would hand a layout-only agent the program every future pane launches — the guarantee decision 0006 exists to make. Anything naming a program, path or destination is Human-only, asserted by a test that reads the path rather than trusting the author. Four paths are permanently not settings, with the reason recorded beside each and a test asserting their absence: the usage endpoint URL, the credentials path, the client identity, and the shell program. A configuration file that can redirect where an access token is sent is a credential exfiltration path with a friendly name. Environment access is a trait rather than std::env, because the workspace forbids unsafe, set_var is unsafe in Rust 2024, and the layering rule has to be testable without mutating the process running the test. Verified live with LUMBRIDGE_CLAUDE_OAUTH=0: the account-endpoint row reads off, greyed, "pinned by LUMBRIDGE_CLAUDE_OAUTH". The Advanced page names every file, endpoint and child process Lumbridge touches and states that nothing is sent anywhere else — as a fact, not as a toggle nobody can flip. File loading, comment-preserving writes and editable controls are not in this pass; 0022 records why that order is the honest one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
179 lines
6.9 KiB
Rust
179 lines
6.9 KiB
Rust
//! The bridge between the derivation engine and GPUI.
|
|
//!
|
|
//! `lumbridge-theme` knows nothing about a renderer. This converts a derived
|
|
//! [`Palette`] into GPUI's colour type once, when the theme changes, and the
|
|
//! shell owns the result. GPUI has no inherited-context mechanism, so a palette
|
|
//! either travels as an argument or lives in one owner; it lives in the shell,
|
|
//! and `ThemeColors` is `Copy` so passing it costs nothing.
|
|
//!
|
|
//! Because the catalog is compiled in, the first frame paints the right theme.
|
|
//! There is no cache to warm, nothing to load asynchronously, and no flash of
|
|
//! the wrong colours on startup.
|
|
|
|
use gpui::Rgba;
|
|
use lumbridge_core::UsageProvenance;
|
|
use lumbridge_theme::{Palette, Provenance, Srgb, ThemeAnchors, accent_or_default, derive};
|
|
|
|
/// A [`Palette`] in the renderer's colour type.
|
|
///
|
|
/// Every field is the same role by the same name. `Rgba` coerces into GPUI's
|
|
/// `Fill` and `Hsla`, so one of these can be handed straight to `.bg()`,
|
|
/// `.text_color()`, or `.border_color()`.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub(crate) struct ThemeColors {
|
|
pub(crate) chrome: Rgba,
|
|
pub(crate) surface: Rgba,
|
|
pub(crate) surface_raised: Rgba,
|
|
pub(crate) surface_active: Rgba,
|
|
/// The settings pane sits on this. The command palette and the panel
|
|
/// chooser still use `surface_raised`; moving them is a visible change and
|
|
/// belongs in a commit that says so.
|
|
pub(crate) surface_overlay: Rgba,
|
|
pub(crate) border: Rgba,
|
|
pub(crate) border_quiet: Rgba,
|
|
pub(crate) text: Rgba,
|
|
pub(crate) muted: Rgba,
|
|
pub(crate) accent: Rgba,
|
|
pub(crate) success: Rgba,
|
|
/// No surface reports failure in colour yet; a dead pane is described in
|
|
/// words. Defined here so the role exists when one does.
|
|
pub(crate) danger: Rgba,
|
|
/// A danger fill quiet enough to sit behind text.
|
|
pub(crate) danger_container: Rgba,
|
|
pub(crate) attention: Rgba,
|
|
/// The needs-input row tint, which arrives with the sidebar rework.
|
|
#[expect(dead_code, reason = "the attention row is rebuilt with the sidebar")]
|
|
pub(crate) attention_wash: Rgba,
|
|
/// The dimming behind a modal. Translucent, so what it covers stays legible
|
|
/// as context rather than disappearing.
|
|
pub(crate) scrim_wash: Rgba,
|
|
}
|
|
|
|
/// Widens an 8-bit channel into the 0..=1 float GPUI wants.
|
|
const fn channel(value: u8) -> f32 {
|
|
value as f32 / 255.0
|
|
}
|
|
|
|
fn rgba(color: Srgb) -> Rgba {
|
|
Rgba {
|
|
r: channel(color.r),
|
|
g: channel(color.g),
|
|
b: channel(color.b),
|
|
a: 1.0,
|
|
}
|
|
}
|
|
|
|
impl From<&Palette> for ThemeColors {
|
|
fn from(palette: &Palette) -> Self {
|
|
Self {
|
|
chrome: rgba(palette.chrome),
|
|
surface: rgba(palette.surface),
|
|
surface_raised: rgba(palette.surface_raised),
|
|
surface_active: rgba(palette.surface_active),
|
|
surface_overlay: rgba(palette.surface_overlay),
|
|
border: rgba(palette.border),
|
|
border_quiet: rgba(palette.border_quiet),
|
|
text: rgba(palette.text),
|
|
muted: rgba(palette.muted),
|
|
accent: rgba(palette.accent),
|
|
success: rgba(palette.success),
|
|
danger: rgba(palette.danger),
|
|
danger_container: rgba(palette.danger_container),
|
|
attention: rgba(palette.attention),
|
|
attention_wash: rgba(palette.attention_wash),
|
|
scrim_wash: Rgba {
|
|
a: 0.55,
|
|
..rgba(palette.scrim)
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The theme every surface reads from.
|
|
///
|
|
/// Owned by the shell rather than installed as a GPUI global: `&self` render
|
|
/// methods far outnumber the ones holding an `App`, and one owner is easier to
|
|
/// reason about than a global plus a cache of it. Switching themes replaces
|
|
/// this value and asks for a redraw.
|
|
pub(crate) struct ActiveTheme {
|
|
/// Both are read by the theme picker, which is not built yet.
|
|
#[allow(dead_code, reason = "read by the theme picker")]
|
|
pub(crate) name: &'static str,
|
|
#[allow(dead_code, reason = "read by the theme picker")]
|
|
pub(crate) is_dark: bool,
|
|
pub(crate) colors: ThemeColors,
|
|
palette: Palette,
|
|
}
|
|
|
|
impl ActiveTheme {
|
|
pub(crate) fn new(anchors: &ThemeAnchors, accent_wire: &str) -> Self {
|
|
let accent = accent_or_default(accent_wire);
|
|
let palette = derive(anchors, accent.resolve(anchors.is_dark(), anchors.fg));
|
|
Self {
|
|
name: anchors.name,
|
|
is_dark: palette.is_dark,
|
|
colors: ThemeColors::from(&palette),
|
|
palette,
|
|
}
|
|
}
|
|
|
|
/// The colour a usage reading is drawn in, by where its number came from.
|
|
///
|
|
/// Decision 0012's rule, enforced by the signature: a provenance goes in
|
|
/// and there is no way to pass a value, so this cannot drift into
|
|
/// "red when nearly exhausted".
|
|
pub(crate) fn provenance(&self, provenance: UsageProvenance) -> Rgba {
|
|
rgba(self.palette.provenance(match provenance {
|
|
UsageProvenance::ProviderReported => Provenance::Provider,
|
|
UsageProvenance::HarnessReported => Provenance::Harness,
|
|
UsageProvenance::LocallyMeasured => Provenance::Local,
|
|
UsageProvenance::Estimated => Provenance::Estimated,
|
|
UsageProvenance::Unavailable => Provenance::Unavailable,
|
|
}))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{ActiveTheme, channel};
|
|
use lumbridge_core::UsageProvenance;
|
|
use lumbridge_theme::LUMBRIDGE_SLATE;
|
|
|
|
#[test]
|
|
fn the_default_theme_converts_to_the_colours_the_interface_used_before() {
|
|
let theme = ActiveTheme::new(&LUMBRIDGE_SLATE, "blue");
|
|
assert!(theme.is_dark);
|
|
// 0x090c12 was the BG constant.
|
|
assert!((theme.colors.chrome.r - channel(0x09)).abs() < f32::EPSILON);
|
|
assert!((theme.colors.chrome.g - channel(0x0c)).abs() < f32::EPSILON);
|
|
assert!((theme.colors.chrome.b - channel(0x12)).abs() < f32::EPSILON);
|
|
assert!((theme.colors.chrome.a - 1.0).abs() < f32::EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn provenance_colours_stay_distinct_after_conversion() {
|
|
let theme = ActiveTheme::new(&LUMBRIDGE_SLATE, "blue");
|
|
// Compared as bit patterns: these are exact conversions of distinct
|
|
// 8-bit values, so an equality test is the right question here and
|
|
// clippy's float-comparison warning does not apply.
|
|
let colors: Vec<[u32; 3]> = [
|
|
UsageProvenance::ProviderReported,
|
|
UsageProvenance::HarnessReported,
|
|
UsageProvenance::LocallyMeasured,
|
|
UsageProvenance::Estimated,
|
|
UsageProvenance::Unavailable,
|
|
]
|
|
.into_iter()
|
|
.map(|provenance| {
|
|
let color = theme.provenance(provenance);
|
|
[color.r.to_bits(), color.g.to_bits(), color.b.to_bits()]
|
|
})
|
|
.collect();
|
|
for (index, first) in colors.iter().enumerate() {
|
|
for second in &colors[index + 1..] {
|
|
assert_ne!(first, second, "two provenance colours collapsed");
|
|
}
|
|
}
|
|
}
|
|
}
|