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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

- **Cards are WebP:** lossless, animated where they move, about a third of the size of the PNGs before, pixel for pixel the same.
- **Decorations** at `/decorations/<name>`: painted banners for README sections, with an optional title (`reading-nook`, `workbench`, `raven-moon`, `enchanted-clearing`, `crystal-owl`, `forest-divider`, `forest-footer`).
- **Decorations by night:** every decoration has a night painting (`time=night`), and the profile builder can switch them with dark mode like the cards.
- **Status page** at [questlog.cc/status](https://questlog.cc/status): whether cards are served, whether stats arrive from GitHub, and whether new users get in right now, with times in UTC.
- **Readable logs** for whoever runs an instance: one plain line per request and per background event, and an admin page behind Cloudflare Access (see "Watching it" in the README).

## 1.0.0

Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ To switch to the night painting for readers in dark mode, use GitHub's `<picture
</picture>
```

The first time a card is requested you'll see a "the scribe is inking your sheet" card for a few seconds while your stats are fetched. Refresh and it's there. Cards update every 12 hours. If GitHub's hourly limit is ever reached, the card says how many minutes the wait is.
The first time a card is requested you'll see a "the scribe is inking your sheet" card for a few seconds while your stats are fetched. Refresh and it's there. Cards update every 12 hours. If GitHub's hourly limit is ever reached, the card says how many minutes the wait is. [questlog.cc/status](https://questlog.cc/status) shows whether the service is up and letting new users in.

## Scenes

Expand All @@ -50,12 +50,23 @@ The gallery shows each scene's Character Sheet in the order of the table.

## Decorations

Painted banners to head a section of your README or divide two, with an optional title in the card title style. They show no stats, so they need no login.
Painted banners to head a section of your README or divide two, with an optional title in the card title style. They show no stats, so they need no login. Like the cards, each one is painted by day and by night.

![The seven decorations, by day on the left and by night on the right](assets/readme/decorations.png)

```md
![About Me](https://questlog.cc/decorations/reading-nook?title=About%20Me)
```

To switch to the night painting for readers in dark mode, add `time=night` to the dark source:

```html
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://questlog.cc/decorations/reading-nook?title=About%20Me&time=night">
<img alt="About Me" src="https://questlog.cc/decorations/reading-nook?title=About%20Me">
</picture>
```

| Decoration | Painting |
|---|---|
| `reading-nook` | a candlelit lectern in the forest; title on the right |
Expand All @@ -66,7 +77,7 @@ Painted banners to head a section of your README or divide two, with an optional
| `forest-divider` | a mossy log, lower than the others; no title unless you give one |
| `forest-footer` | glowing mushrooms and ferns, as low as the divider; for the end of a README |

Decorations take only `title`, `title_color`, `outline_color`, `truncate`, `animate` and `scale`; any other option draws an error card.
Decorations take only `title`, `title_color`, `outline_color`, `truncate`, `animate`, `scale` and `time` (`time=night` for the night painting); any other option draws an error card.

## Options

Expand Down
Binary file added art/decorations/crystal-owl/0-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/crystal-owl/1-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/crystal-owl/10-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/crystal-owl/11-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/crystal-owl/2-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/crystal-owl/3-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/crystal-owl/4-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/crystal-owl/5-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/crystal-owl/6-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/crystal-owl/7-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/crystal-owl/8-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/crystal-owl/9-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/enchanted-clearing/0-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/enchanted-clearing/1-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/enchanted-clearing/10-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/enchanted-clearing/11-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/enchanted-clearing/2-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/enchanted-clearing/3-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/enchanted-clearing/4-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/enchanted-clearing/5-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/enchanted-clearing/6-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/enchanted-clearing/7-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/enchanted-clearing/8-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/enchanted-clearing/9-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/forest-divider/0-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/decorations/forest-divider/1-night.png
Binary file added art/decorations/forest-divider/10-night.png
Binary file added art/decorations/forest-divider/11-night.png
Binary file added art/decorations/forest-divider/2-night.png
Binary file added art/decorations/forest-divider/3-night.png
Binary file added art/decorations/forest-divider/4-night.png
Binary file added art/decorations/forest-divider/5-night.png
Binary file added art/decorations/forest-divider/6-night.png
Binary file added art/decorations/forest-divider/7-night.png
Binary file added art/decorations/forest-divider/8-night.png
Binary file added art/decorations/forest-divider/9-night.png
Binary file added art/decorations/forest-footer/0-night.png
Binary file added art/decorations/forest-footer/1-night.png
Binary file added art/decorations/forest-footer/10-night.png
Binary file added art/decorations/forest-footer/11-night.png
Binary file added art/decorations/forest-footer/2-night.png
Binary file added art/decorations/forest-footer/3-night.png
Binary file added art/decorations/forest-footer/4-night.png
Binary file added art/decorations/forest-footer/5-night.png
Binary file added art/decorations/forest-footer/6-night.png
Binary file added art/decorations/forest-footer/7-night.png
Binary file added art/decorations/forest-footer/8-night.png
Binary file added art/decorations/forest-footer/9-night.png
Binary file added art/decorations/raven-moon/0-night.png
Binary file added art/decorations/raven-moon/1-night.png
Binary file added art/decorations/raven-moon/10-night.png
Binary file added art/decorations/raven-moon/11-night.png
Binary file added art/decorations/raven-moon/2-night.png
Binary file added art/decorations/raven-moon/3-night.png
Binary file added art/decorations/raven-moon/4-night.png
Binary file added art/decorations/raven-moon/5-night.png
Binary file added art/decorations/raven-moon/6-night.png
Binary file added art/decorations/raven-moon/7-night.png
Binary file added art/decorations/raven-moon/8-night.png
Binary file added art/decorations/raven-moon/9-night.png
Binary file added art/decorations/reading-nook/0-night.png
Binary file added art/decorations/reading-nook/1-night.png
Binary file added art/decorations/reading-nook/10-night.png
Binary file added art/decorations/reading-nook/11-night.png
Binary file added art/decorations/reading-nook/2-night.png
Binary file added art/decorations/reading-nook/3-night.png
Binary file added art/decorations/reading-nook/4-night.png
Binary file added art/decorations/reading-nook/5-night.png
Binary file added art/decorations/reading-nook/6-night.png
Binary file added art/decorations/reading-nook/7-night.png
Binary file added art/decorations/reading-nook/8-night.png
Binary file added art/decorations/reading-nook/9-night.png
Binary file added art/decorations/workbench/0-night.png
Binary file added art/decorations/workbench/1-night.png
Binary file added art/decorations/workbench/10-night.png
Binary file added art/decorations/workbench/11-night.png
Binary file added art/decorations/workbench/2-night.png
Binary file added art/decorations/workbench/3-night.png
Binary file added art/decorations/workbench/4-night.png
Binary file added art/decorations/workbench/5-night.png
Binary file added art/decorations/workbench/6-night.png
Binary file added art/decorations/workbench/7-night.png
Binary file added art/decorations/workbench/8-night.png
Binary file added art/decorations/workbench/9-night.png
Binary file added assets/readme/decorations.png
38 changes: 34 additions & 4 deletions site/app/wiki/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,19 @@ const TOC = [
const WIDGETS: Widget[] = ["stats", "languages", "streak"];


function Shot({ src, alt, scale = 2, h = 128 }: { src: string; alt: string; scale?: number; h?: number }) {
/** A live card that links to its full size; with `dark`, readers in dark mode see that address instead. */
function Shot({ src, alt, scale = 2, h = 128, dark }: { src: string; alt: string; scale?: number; h?: number; dark?: string }) {
const img = <img className="pix" src={snap(`${src}&scale=${scale}`)} width={320 * scale} height={h * scale} alt={alt} loading="lazy" />;
return (
<a className={w.shot} href={src}>
<img className="pix" src={snap(`${src}&scale=${scale}`)} width={320 * scale} height={h * scale} alt={alt} loading="lazy" />
{dark ? (
<picture>
<source media="(prefers-color-scheme: dark)" srcSet={snap(`${dark}&scale=${scale}`)} />
{img}
</picture>
) : (
img
)}
</a>
);
}
Expand All @@ -50,6 +59,10 @@ const PICTURE = `<picture>
<source media="(prefers-color-scheme: dark)" srcset="${HOST}/you/stats/forest/night">
<img alt="My Character Sheet" src="${HOST}/you/stats/forest">
</picture>`;
const DECO_PICTURE = `<picture>
<source media="(prefers-color-scheme: dark)" srcset="${HOST}/decorations/raven-moon?title=Connect&time=night">
<img alt="Connect" src="${HOST}/decorations/raven-moon?title=Connect">
</picture>`;

const STATS_ROWS = [
["Level", "level", "From your lifetime contributions. The bar under it fills toward the next level."],
Expand Down Expand Up @@ -198,6 +211,8 @@ export default function Wiki() {
<p className={w.note}>
Anywhere that renders HTML works the same way: a repository README, a wiki or your own site. Where only
Markdown images are allowed, use <code>![My stats]({HOST}/stats?user=you)</code> and pick one time.
Decorations switch the same way: put <code>?time=night</code> on the dark one (see{" "}
<a href="#decorations">Decorations</a>).
</p>
</section>

Expand Down Expand Up @@ -365,14 +380,29 @@ export default function Wiki() {
<Code>{`![About Me](${HOST}/decorations/reading-nook?title=About%20Me)`}</Code>
<p>
Decorations take only <code>title</code>, <code>title_color</code>, <code>outline_color</code>,{" "}
<code>truncate</code>, <code>animate</code> and <code>scale</code>. A title too long for its place is
trimmed with &quot;...&quot;; <code>truncate=false</code> draws it whole.
<code>truncate</code>, <code>animate</code>, <code>scale</code> and <code>time</code>. A title too long
for its place is trimmed with &quot;...&quot;; <code>truncate=false</code> draws it whole.{" "}
<code>time=night</code> draws the night painting. The ones below switch with your theme; to do the same in
your README, use a <code>&lt;picture&gt;</code> tag (the builder writes it for you when you pick Switch
with dark mode):
</p>
<Code>{DECO_PICTURE}</Code>
<div className={w.pair}>
<figure>
<Shot src="/decorations/raven-moon?title=Connect" alt="The Raven Moon decoration by day" />
<figcaption>Light theme</figcaption>
</figure>
<figure>
<Shot src="/decorations/raven-moon?title=Connect&time=night" alt="The Raven Moon decoration by night" />
<figcaption>Dark theme</figcaption>
</figure>
</div>
<div className={w.grid2}>
{DECORATIONS.map(({ name, label, title, h }) => (
<figure key={name}>
<Shot
src={`/decorations/${name}?title=${encodeURIComponent(title)}`}
dark={`/decorations/${name}?title=${encodeURIComponent(title)}&time=night`}
alt={title ? `The ${label} decoration titled ${title}` : `The ${label} decoration`}
h={h}
/>
Expand Down
35 changes: 30 additions & 5 deletions site/components/builder/CardOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,26 +103,51 @@ function Foot({ onRemove, onClose }: Pick<Props, "onRemove" | "onClose">) {
);
}

/** A decoration has no stats and no night: the painting, its title and the title's colors. */
/** A decoration has no stats: the painting, day or night, its title and the title's colors. */
function DecorationOptions({ spot, index, count, onChange, onRemove, onClose, onStep }: Props) {
const c = spot.card;
const set = (key: string, value: string, typing = true) => onChange(setParam(c, key, value), "single", typing);
const mode: Mode = spot.kind === "picture" && spot.dark ? "auto" : "single";
const time: Time = mode === "auto" ? "auto" : c.night ? "night" : "day";
const set = (key: string, value: string, typing = true) => onChange(setParam(c, key, value), mode, typing);
const setTime = (t: Time) => onChange({ ...c, night: t === "night" }, t === "auto" ? "auto" : "single");
const title = getParam(c, "title");
return (
<div className={s.opts}>
<Head name="Decoration" sub={decorationLabel(c.scene)} index={index} count={count} onStep={onStep} onClose={onClose} />
<Head
name="Decoration"
sub={`${decorationLabel(c.scene)}, ${time === "auto" ? "switches with dark mode" : time === "night" ? "by night" : "by day"}`}
index={index}
count={count}
onStep={onStep}
onClose={onClose}
/>
<fieldset className={s.group}>
<legend className={s.label}>Painting</legend>
<div className={`${s.thumbs} ${s.c3}`}>
{DECORATIONS.map((d) => (
<label key={d.name} className={s.thumb}>
<input type="radio" name={`deco-${index}`} checked={c.scene === d.name} onChange={() => onChange({ ...c, scene: d.name }, "single")} />
<img className="pix" src={decorationThumb(d.name)} width={640} height={d.h * 2} alt="" loading="lazy" />
<input type="radio" name={`deco-${index}`} checked={c.scene === d.name} onChange={() => onChange({ ...c, scene: d.name }, mode)} />
<img className="pix" src={decorationThumb(d.name, time === "night")} width={640} height={d.h * 2} alt="" loading="lazy" />
<span className={s.thumbScene}>{d.label}</span>
</label>
))}
</div>
</fieldset>
<Segmented<Time>
label="Time of day"
value={time}
onChange={setTime}
options={[
{ value: "day", label: "Day", icon: <Sun aria-hidden="true" /> },
{ value: "night", label: "Night", icon: <Moon aria-hidden="true" /> },
{ value: "auto", label: "Switch with dark mode", icon: <SunMoon aria-hidden="true" /> },
]}
help={
time === "auto"
? "Readers who use GitHub in dark mode see the night painting, everyone else the day one."
: "Pick Switch with dark mode to show night to readers who use GitHub in dark mode."
}
/>
<Field label="Title" value={title} onChange={(v) => set("title", v)} help="Empty: the painting alone. Example: About Me" />
<details className={s.more}>
<summary>
Expand Down
15 changes: 13 additions & 2 deletions site/components/builder/ProfileBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -408,8 +408,19 @@ export default function ProfileBuilder() {

const addDecoration = (name: string) => {
const d = DECORATIONS.find((x) => x.name === name);
const card: Card = { origin: env.origin, user: "", widget: "decorations", scene: name, night: false, params: d?.title ? [["title", d.title]] : [], hash: "" };
const r = insertAt(text, cursor.current ?? text.length, writeSpot(card, autoAlt(card), "single"));
// like a card: switches with dark mode, or stays at night, when the last image in the README does
const last = spots[spots.length - 1];
const mode: Mode = last && last.kind === "picture" && last.dark ? "auto" : "single";
const card: Card = {
origin: env.origin,
user: "",
widget: "decorations",
scene: name,
night: mode === "single" && !!last?.card.night,
params: d?.title ? [["title", d.title]] : [],
hash: "",
};
const r = insertAt(text, cursor.current ?? text.length, writeSpot(card, autoAlt(card), mode));
edit(r.text, { now: true });
cursor.current = r.end;
setFlash(findSpots(r.text, env.host).findIndex((x) => x.start === r.start));
Expand Down
11 changes: 6 additions & 5 deletions site/components/builder/cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ export const sceneLabel = sceneName;
export const kindName = (c: Card) => (c.widget === "decorations" ? decorationLabel(c.scene) : cardName(c.widget, c.scene));

/** A decoration's small picture, without a title. */
export const decorationThumb = (name: string) => snap(decorationThumbUrl(name));
const decorationThumbUrl = (name: string) => `/decorations/${name}?scale=2&animate=false`;
export const decorationThumb = (name: string, night = false) => snap(decorationThumbUrl(name, night));
const decorationThumbUrl = (name: string, night = false) => `/decorations/${name}?scale=2&animate=false${night ? "&time=night" : ""}`;

/** The demo card a picker shows as a small picture: the site's own copy, at scale 2. */
export function thumb(w: Widget, scene: string, night = false, fire = "") {
Expand All @@ -68,7 +68,7 @@ export const THUMBS = [
...WIDGETS.flatMap((w) =>
SCENES.flatMap((sc) => [false, true].flatMap((night) => (w === "streak" ? ["themed", ""] : [""]).map((f) => thumbUrl(w, sc, night, f)))),
),
...DECORATIONS.map((d) => decorationThumbUrl(d.name)),
...DECORATIONS.flatMap((d) => [decorationThumbUrl(d.name), decorationThumbUrl(d.name, true)]),
];

/** A card address, or null for anything that is not one. Reads the short form /user/card/scene/time and the long
Expand All @@ -89,7 +89,8 @@ export function parseCard(src: string, ownHost = ""): Card | null {
}
const q = u.searchParams;
if (parts.length === 2 && parts[0] === "decorations") {
return { origin: u.origin, user: "", widget: "decorations", scene: parts[1], night: false, params: [...q.entries()], hash: u.hash };
const params = [...q.entries()].filter(([k]) => k !== "time");
return { origin: u.origin, user: "", widget: "decorations", scene: parts[1], night: q.get("time") === "night", params, hash: u.hash };
}
let user: string | null;
let widget: string;
Expand Down Expand Up @@ -125,7 +126,7 @@ export function enc(v: string) {
/** The short address: the scene and night as path parts when they differ from the defaults, the rest as a query. */
export function writeCard(c: Card, night = c.night): string {
if (c.widget === "decorations") {
const query = c.params.map(([k, v]) => `${k}=${enc(v)}`).join("&");
const query = [...c.params, ...(night ? [["time", "night"]] : [])].map(([k, v]) => `${k}=${enc(v)}`).join("&");
return `${c.origin}/decorations/${enc(c.scene)}${query ? `?${query}` : ""}${c.hash}`;
}
const path = night ? `/${c.scene}/night` : c.scene !== "forest" ? `/${c.scene}` : "";
Expand Down
1 change: 1 addition & 0 deletions site/components/home/Decorations.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
}

.frame {
display: block;
overflow: hidden;
border-radius: 18px;
box-shadow:
Expand Down
6 changes: 4 additions & 2 deletions site/components/home/Decorations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ export default function Decorations() {
</h2>
<p className="lede">
Painted banners to head the sections of your README, or to divide them. Write any title you like, or none.
Each one is painted by day and by night, and switches with the reader&apos;s theme like the cards.
</p>
</div>
<div className={s.grid}>
{DECORATIONS.map((d) => (
<figure key={d.name} className={d.h < 128 ? `${s.shot} ${s.wide}` : s.shot}>
<div className={s.frame}>
<picture className={s.frame}>
<source media="(prefers-color-scheme: dark)" srcSet={snap(`${address(d.name, d.title)}${d.title ? "&" : "?"}scale=2&time=night`)} />
<img
className="pix"
src={snap(`${address(d.name, d.title)}${d.title ? "&" : "?"}scale=2`)}
Expand All @@ -26,7 +28,7 @@ export default function Decorations() {
loading="lazy"
alt={d.title ? `The ${d.label} decoration titled ${d.title}` : `The ${d.label} decoration`}
/>
</div>
</picture>
<figcaption>
<strong className="display">{d.label}</strong>
<code>{address(d.name, d.title).replace(/%20/g, " ")}</code>
Expand Down
2 changes: 1 addition & 1 deletion site/components/snapped.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version": "1397ef4b86"}
{"version": "7c96af3f45"}
Binary file modified site/public/cards/stats.user=BaranDev,scale=1.webp
21 changes: 18 additions & 3 deletions src/decorations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::scene::{Align, Assets};
use crate::widgets::Drawn;
use std::collections::HashMap;

pub const OPTIONS: [&str; 6] = ["title", "title_color", "outline_color", "truncate", "animate", "scale"];
pub const OPTIONS: [&str; 7] = ["title", "title_color", "outline_color", "truncate", "animate", "scale", "time"];

#[derive(Debug, Clone, PartialEq)]
pub struct Options {
Expand All @@ -18,11 +18,13 @@ pub struct Options {
pub truncate: bool,
pub animate: bool,
pub scale: u8,
/// `time=night`: the night painting, for a dark theme.
pub night: bool,
}

impl Default for Options {
fn default() -> Options {
Options { title: String::new(), title_color: None, outline_color: None, truncate: true, animate: true, scale: 5 }
Options { title: String::new(), title_color: None, outline_color: None, truncate: true, animate: true, scale: 5, night: false }
}
}

Expand All @@ -38,6 +40,11 @@ pub fn parse(q: &HashMap<String, String>) -> Result<Options, String> {
Some("false") | Some("0") => Ok(false),
Some(v) => Err(format!("Unknown {k} '{}' (use true or false)", clip(v))),
};
o.night = match q.get("time").map(String::as_str) {
None | Some("day") => false,
Some("night") => true,
Some(t) => return Err(format!("Unknown time '{}' (use day or night)", clip(t))),
};
o.truncate = switch("truncate")?;
o.animate = switch("animate")?;
if let Some(v) = q.get("scale") {
Expand All @@ -58,7 +65,8 @@ pub fn parse(q: &HashMap<String, String>) -> Result<Options, String> {
/// each frame shows. None for an unknown name.
pub fn render(assets: &Assets, name: &str, o: &Options) -> Option<(Drawn, u16)> {
let d = assets.decorations.get(name)?;
let mut frames = if o.animate { d.frames.clone() } else { d.frames[..1].to_vec() };
let painting = if o.night && !d.night.is_empty() { &d.night } else { &d.frames };
let mut frames = if o.animate { painting.clone() } else { painting[..1].to_vec() };
let (w, h) = (frames[0].w, frames[0].h);
let mut title = (o.scale >= 2).then(|| Image::new(2 * w, 2 * h));
let line = if o.truncate { fit(&assets.title_font, &o.title, d.title.w) } else { o.title.clone() };
Expand Down Expand Up @@ -98,6 +106,8 @@ mod tests {
assert!(e.starts_with("Decorations take title,") && e.ends_with("not 'user'"), "{e}");
assert!(parse(&q(&[("scale", "9")])).unwrap_err().starts_with("Unknown scale '9'"));
assert!(parse(&q(&[("outline_color", "red")])).unwrap_err().starts_with("Bad outline_color"));
assert!(parse(&q(&[("time", "night")])).unwrap().night);
assert!(parse(&q(&[("time", "noon")])).unwrap_err().starts_with("Unknown time 'noon'"));
}

#[test]
Expand All @@ -115,6 +125,11 @@ mod tests {
assert!(coarse.title.is_none() && coarse.frames.len() == 1 && coarse.frames[0] != plain.frames[0], "{name}: scale 1");
}
assert!(render(&a, "nope", &Options::default()).is_none());
for (name, d) in &a.decorations {
assert_eq!(d.night.len(), d.frames.len(), "{name} has a night painting");
let (night, _) = render(&a, name, &Options { night: true, ..Default::default() }).unwrap();
assert_ne!(night.frames[0], render(&a, name, &Options::default()).unwrap().0.frames[0], "{name}: night differs");
}
}

#[test]
Expand Down
Loading
Loading