feat(rhema-code): add the colors setting with four palettes
The stored `colors` key names a palette — rhema (the default, Jadeite), aquamarine, black-and-white, or dracula — and the Appearance page of /settings cycles it live, like the theme and icon toggles beside it. Each palette carries a dark and a light appearance under the existing `theme` key; dracula's light side is derived, the official palette ships only dark. The appearance decision moves off token comparison: Theme now answers is_dark() from the ground/text luminance, so palettes beyond the original two get the right per-profile fallback tones.
This commit is contained in:
commit
81da26b955
159 files changed
+50628
No files matched your search
@@ -0,0 +1,89 @@
|
||||
//! The calendar client contract: one trait, one call.
|
||||
//!
|
||||
//! [`Calendar`] is the whole surface a consumer needs: the occurrences
|
||||
//! starting inside a window. Implementations are cheap to clone and
|
||||
//! share (the reqwest client inside is reference-counted) and
|
||||
//! object-safe so a backend can be swapped without generics.
|
||||
//!
|
||||
//! Polls are at-least-once and window-anchored. Nothing here tracks a
|
||||
//! cursor, because a calendar is addressable by time, not by history:
|
||||
//! re-asking the same window returns the same rows under the same
|
||||
//! ids, and the consumer's dispatcher dedups the repetition. There is
|
||||
//! deliberately no write half: reminders read a calendar; what a
|
||||
//! consumer does about one is an agent's business, not an adapter's.
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::error::CalendarError;
|
||||
use crate::event::{CalendarEvent, Window};
|
||||
|
||||
/// A calendar backend: the client for one feed dialect, bound to one
|
||||
/// calendar (the URL for ICS, the calendar id for Google).
|
||||
#[async_trait]
|
||||
pub trait Calendar: Send + Sync {
|
||||
/// The occurrences whose start falls in `window`, oldest first,
|
||||
/// ties broken by id. Recurring series arrive expanded: one
|
||||
/// [`CalendarEvent`] per occurrence, overrides applied, cancelled
|
||||
/// occurrences gone.
|
||||
async fn events(&self, window: &Window) -> Result<Vec<CalendarEvent>, CalendarError>;
|
||||
}
|
||||
|
||||
/// Folds collected events into an adapter's answer: keeps the rows
|
||||
/// starting in `window`, drops duplicate ids (first wins), and sorts
|
||||
/// by `(start, id)`. Adapters call this before returning, so a
|
||||
/// consumer reads one deterministic order regardless of which feed
|
||||
/// produced the rows.
|
||||
pub(crate) fn finish(mut events: Vec<CalendarEvent>, window: &Window) -> Vec<CalendarEvent> {
|
||||
events.retain(|event| window.starts_within(event.start));
|
||||
events.sort_by(|a, b| (&a.start, &a.id).cmp(&(&b.start, &b.id)));
|
||||
events.dedup_by(|a, b| a.id == b.id);
|
||||
events
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn event(id: &str, start: &str) -> CalendarEvent {
|
||||
CalendarEvent {
|
||||
id: id.to_owned(),
|
||||
title: "x".to_owned(),
|
||||
start: time::OffsetDateTime::parse(
|
||||
start,
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.expect("parses"),
|
||||
end: None,
|
||||
all_day: false,
|
||||
location: None,
|
||||
description: None,
|
||||
attendees: Vec::new(),
|
||||
organizer: None,
|
||||
url: None,
|
||||
raw: json!({}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finish_clips_sorts_and_dedups() {
|
||||
let window = Window::new(at("2026-10-20T00:00:00Z"), at("2026-10-21T00:00:00Z"));
|
||||
let events = finish(
|
||||
vec![
|
||||
event("b", "2026-10-20T10:00:00Z"),
|
||||
event("z", "2026-10-19T10:00:00Z"), // before: dropped
|
||||
event("a", "2026-10-20T10:00:00Z"),
|
||||
event("b", "2026-10-20T10:00:00Z"), // duplicate id: dropped
|
||||
event("c", "2026-10-21T00:00:00Z"), // at `to`: dropped
|
||||
],
|
||||
&window,
|
||||
);
|
||||
let ids: Vec<&str> = events.iter().map(|event| event.id.as_str()).collect();
|
||||
assert_eq!(ids, ["a", "b"]);
|
||||
}
|
||||
|
||||
fn at(raw: &str) -> time::OffsetDateTime {
|
||||
time::OffsetDateTime::parse(raw, &time::format_description::well_known::Rfc3339)
|
||||
.expect("parses")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user