palettes.rs

crates/veilvoice-gui/src/palettes.rs

veilvoice-gui · 700 lines · read the source here · or on GitHub

User-defined colour schemes, and the contrast check that keeps them usable.

A reader can drop a small text file into a palettes/ directory beside the preferences file and have it appear in the theme picker alongside the nine built-in schemes. The format is the same key = value shape crate::prefs already uses, for the same reason: it is trivial to write by hand, trivial to parse without a dependency, and has no syntax in which something surprising can hide.

A palette file is untrusted input

It arrives from the filesystem, it may have been written by hand at two in the morning, and it may have been copied from a web page by somebody who has never seen a hex colour. So it is parsed like anything else this project reads: every field validated, every failure named, and refused rather than patched up.

Refusing matters more here than it first appears. The obvious lenient design is to fill in whatever is missing from the default theme -- and that produces a palette which is mostly the user's, with a few colours from somewhere else, and no indication which. The user sees an application that looks subtly wrong and has nothing to go on. An error naming the missing token is worth more than a window that opens.

Contrast is computed, not trusted

The request that prompted this asked for "whatever colour with correct contrast to read". Correct contrast is not an aesthetic judgement, it is arithmetic: WCAG 2.1 defines relative luminance and a contrast ratio, and a ratio below 4.5 means body text a substantial number of people cannot read.

So a palette whose foreground fails against its own background is refused with the measured ratio in the message, rather than accepted and left to produce an application nobody can use. It is the one validation here that is about the user rather than about the parser, and it is the reason this module exists at all rather than the fields being read straight into a struct.

The thresholds are stated where they are enforced, and they are the standard's, not invented here:

| Pair | Minimum | Why | |---|---|---| | fg on bg | 4.5 | Body text, WCAG AA | | fg on bg_soft | 4.5 | The same text on a raised surface | | muted on bg | 3.0 | Secondary text, AA large-text threshold | | accent on bg | 3.0 | Links and controls, AA non-text contrast | | err on bg | 3.0 | A warning nobody can read is worse than none |

muted is deliberately held to 3.0 rather than 4.5. It is used for secondary text that is meant to recede, every built-in theme would fail at 4.5, and pretending otherwise would mean shipping a rule the project's own themes break.

In plain words

Lets you write your own colour scheme and have VeilVoice use it.

Drop a small text file in a folder and it appears in the list. Every colour has to be there, and the text has to be readable against the background: a scheme whose text fails that check is refused rather than applied, with the measured numbers so you know how far off it is and which way to move.

WHAT THIS FILE CONTAINS

700 lines defining 8 functions (4 public), 1 type and 4 constants. Everything below is read out of the source, so it cannot disagree with the code.

The types it owns.

  • struct Parsed line 156 · One parsed colour scheme, before it is accepted.

What happens when it runs. These are the ways in: public, and nothing else in this file calls them, so they are what an outside caller reaches first.

  • default_dir line 291 · Where palettes live, beside the preferences file.
  • load line 318 · Read every palette in dir, returning the usable ones and every complaint.
    reaches build, contrast_problems, parse, contrast, parse_hex, luminance

WHAT CALLS WHAT

luminance line 94 contrast line 110 contrast_problems line 130 parse_hex line 163 parse line 180 build line 258 default_dir line 291 load line 318 entry: a way in: public, and nothing in this file calls it api: public, and also used inside this file helper: private to this file dashed: a call that goes back up, or across a wrapped rank The functions this file defines, and the calls between them. An edge means the callee's name appears, called, inside the caller's body. This is a syntactic reading, not a type-resolved one.

The functions this file defines, and the calls between them. An edge means the callee's name appears, called, inside the caller's body. This is a syntactic reading, not a type-resolved one.

The same graph as Mermaid source
%%{init: {"theme":"base","themeVariables":{"background":"#1a1b26","primaryColor":"#1f2335","primaryTextColor":"#c0caf5","primaryBorderColor":"#7aa2f7","secondaryColor":"#16161e","tertiaryColor":"#16161e","lineColor":"#737aa2","textColor":"#c0caf5","mainBkg":"#1f2335","nodeBorder":"#7aa2f7","clusterBkg":"#16161e","clusterBorder":"#2f3549","fontFamily":"ui-monospace, SFMono-Regular, Consolas, monospace","fontSize":"14px"}}}%%
flowchart TD
    n_luminance["luminance<br/>line 94"]
    n_contrast["contrast<br/>line 110"]
    n_contrast_problems["contrast_problems<br/>line 130"]
    n_parse_hex["parse_hex<br/>line 163"]
    n_parse["parse<br/>line 180"]
    n_build["build<br/>line 258"]
    n_default_dir(["default_dir<br/>line 291"])
    n_load(["load<br/>line 318"])
    n_contrast --> n_luminance
    n_contrast_problems --> n_contrast
    n_load --> n_build
    n_load --> n_contrast_problems
    n_load --> n_parse
    n_parse --> n_parse_hex
    click n_luminance href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/palettes.rs#L94" "open the source"
    click n_contrast href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/palettes.rs#L110" "open the source"
    click n_contrast_problems href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/palettes.rs#L130" "open the source"
    click n_parse_hex href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/palettes.rs#L163" "open the source"
    click n_parse href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/palettes.rs#L180" "open the source"
    click n_build href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/palettes.rs#L258" "open the source"
    click n_default_dir href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/palettes.rs#L291" "open the source"
    click n_load href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/palettes.rs#L318" "open the source"
    classDef entry fill:#1f2335,stroke:#7aa2f7,color:#c0caf5
    class n_default_dir,n_load entry
    classDef api fill:#1f2335,stroke:#7dcfff,color:#c0caf5
    class n_contrast,n_contrast_problems api
    classDef helper fill:#1f2335,stroke:#bb9af7,color:#c0caf5
    class n_luminance,n_parse_hex,n_parse,n_build helper

This site loads no third-party script, so it cannot run Mermaid; the diagram above is the same nodes and edges drawn by the generator instead. GitHub renders the source below directly.

ITEMS

ItemLineDocumentation
REQUIRED pub const73Every token a palette file has to define.
MAX_PALETTES pub const84The most palette files that will be read from the directory.
MAX_BYTES pub const87The largest palette file that will be read.
luminance fn94Relative luminance, as defined by WCAG 2.1.
contrast pub fn110The WCAG contrast ratio between two colours, from 1.0 to 21.0.
PAIRS const117The contrast pairs a palette has to satisfy, with the reason for each.
contrast_problems pub fn130Check a palette's contrast, returning one message per failing pair.
Parsed struct156One parsed colour scheme, before it is accepted.
parse_hex fn163
parse fn180Parse a palette file's text.
build fn258
default_dir pub fn291Where palettes live, beside the preferences file.
load pub fn318Read every palette in dir, returning the usable ones and every complaint.