dotfiles
Desktop

The verified calendar

Edit on GitHub

The calendar the bar opens from the time and the date. Since 18/10/2026 its holidays come from a holiday registry: structured, versioned, sourced and tested, instead of a hand list inside Bar.qml. The registry lives in modules/home/desktop/calendar/; the panel is modules/home/desktop/quickshell/bar/CalendarPopover.qml.

official sources ──► registry/*.json (by hand, reviewed) ──► registry.py build (Nix, at build time)
                                                                  │
                     ~/.config/theme/calendar.json ◄──────────────┘
                                  │
                     Quickshell (CalendarModel) ──► CalendarPopover.qml, the band's month card

Nothing reads the network when the panel opens: the data is a file Nix wrote. A source that changes is noticed by a weekly workflow, which never edits the data by itself (below).

The model

Each event keeps apart four things a calendar website usually blends:

FieldWhat it saysValues
ruleWHENfixed (month, day), easter (offset from Easter Sunday), date (one dated act), range (several days)
jurisdictionWHEREBR, BR-SP, BR-SP-SAO-CARLOS, UN
kindWHAT it is in lawcivil_holiday, religious_holiday, optional (ponto facultativo), optional_partial, observance
applicabilityFOR WHOMgeneral, federal_admin, state_admin, municipal_admin, municipal_education, observance

Plus status, verified_at (the day the source was actually read), basis (the act and article), sources (ids into the source table, each with title, URL, authority, document id and when it was checked), years (when a law took effect), confirmed (per-year confirmation by an annual act, with its own sources and hours), annual_act, partial ({"until": "14:00"}), notes.

An optional day is never general. A ponto facultativo in the city hall's portaria frees the city's own staff, and the federal one frees federal staff; neither gives a private employee a day off. The validator refuses an optional day marked general, and a UN date that is anything but an observance.

The same day can hold two records, on purpose. Corpus Christi is a municipal holiday in São Carlos AND a federal optional day; Cinzas is optional until 12:00 for the city and until 14:00 for the federal administration. They are different facts from different acts, so both are kept. Inheritance never duplicates: a national holiday is one record, valid wherever you are.

Status

StatusMeansExample
verifiedread in an official source on verified_atTiradentes (Lei 662/1949)
calculateda law fixes the holiday; the DATE of that year comes from the computusCorpus Christi 2027
pendingthe date is computable but an annual act must still declare itfederal Carnaval 2027
stalethe source moved or could not be re-read in time(none today)
disputedofficial sources disagree in a way that changes the answer(none today)

occurrence_status() derives the per-year status: a year an act confirmed takes the act's sources; an annual_act event without one is pending; an Easter-based law holiday is calculated outside a confirmed year. A future year never inherits a ponto facultativo.

Dates

Civil dates only (datetime.date): no time of day, so no time zone or daylight saving can move a holiday. Easter is the anonymous Gregorian algorithm, tested against published dates (2000, 2019, 2024 to 2028, 2038). A fixed 29/02 has no occurrence in a common year instead of a shifted one, and Carnaval's Tuesday on 29/02/2028 is a test of its own.

The sources (read 18/10/2026)

SourceRead howResult
Prefeitura de São Carlos, feriadoscurl, the chain completed through AIA (the server omits its intermediate)the 2026 list, citing Portaria 2083/2025 and Lei 10.257/1990
Portaria nº 2083/2025the PDF's text25 items, for the municipal executive's own staff
Lei Municipal nº 10.257/1990a scan, read by OCRthe four religious holidays: Sexta-Feira da Paixão, Corpus Christi, São Carlos Borromeu, 15 de Agosto
Portaria MGI nº 11.460/2025the DOU's text through Sigepe Legisnational holidays and federal optional days of 2026
Lei 662/1949, 6.802/1980, 9.093/1995, 14.759/2023Planalto, revoked wording skippedthe national civil holidays; religious ones are municipal law
Lei SP 9.497/1997ALESP answered 503 to two clientsNOT re-read; 9 July confirmed by the municipal portaria
UN, International Days and Weeksthe page's rows, imported248 observances for 2026 (249 rows, one listed twice)

A search on 18/10/2026 found no later act changing Portaria 2083/2025.

What the audit corrected

  • Good Friday is not a national holiday by law. The old list had it as national. Lei 9.093/1995, art. 2º, makes it a religious holiday declared in MUNICIPAL law, and São Carlos declares it. The MGI portaria still calls it "feriado nacional": the record keeps both sources and the note, and it is a holiday in São Carlos in either reading.
  • Corpus Christi's basis is Lei 10.257/1990, which revoked Lei 10.219/1989 and the contrary provisions. The old note cited a Lei 7.502/1974 for which no source was found.
  • 04/11 is the patron's religious holiday (São Carlos Borromeu, Lei 10.257/1990), published by the city as its anniversary. Both names are recorded.
  • Carnaval and Cinzas are optional days for two administrations, with different hours, never a holiday and never for everyone. The old list called them national optional days.
  • Teachers' Day (15/10) applies only to the municipal school network, and its underlying law was not found, so it is recorded for 2026 only, as published.
  • The bridge days (20/04, 05/06, 10/07, 03/11) and the year-end ones are the 2026 acts' own, and are not projected into other years.

The 2026 audit

The test Audit2026 asserts every item of Portaria 2083/2025 against the expansion: its date, kind, jurisdiction and applicability, and that it is verified. It also asserts 14 general holidays in São Carlos in 2026, the city's own count.

DateEventKindForSourceStatus
01/01Confraternização Universalcivil holidayeveryoneLei 662/1949verified
16/02, 17/02Carnavaloptionalmunicipal staff; federal staffPortaria 2083/2025; Portaria MGI 11.460/2025verified
18/02Cinzasoptional until 12:00 / 14:00municipal / federal staffsameverified
03/04Paixão de Cristoreligious holidayeveryoneLei 10.257/1990 (and both portarias)verified
20/04(bridge)optionalmunicipal and federal staffboth portariasverified
21/04Tiradentescivil holidayeveryoneLei 662/1949verified
01/05Dia Mundial do Trabalhocivil holidayeveryoneLei 662/1949verified
04/06Corpus Christireligious holiday; also a federal optional dayeveryone; federal staffLei 10.257/1990; Portaria MGIverified
05/06(bridge)optionalmunicipal and federal staffboth portariasverified
09/07Revolução Constitucionalistacivil holiday (state)everyoneLei SP 9.497/1997 via Portaria 2083verified (law not re-read)
10/07(bridge)optionalmunicipal staffPortaria 2083/2025verified
15/08Nossa Senhora da Babilôniareligious holidayeveryoneLei 10.257/1990verified
07/09Independência do Brasilcivil holidayeveryoneLei 662/1949verified
12/10Nossa Senhora Aparecidacivil holidayeveryoneLei 6.802/1980verified
15/10Dia do Professorholidaymunicipal school network onlyPortaria 2083/2025verified
28/10Dia do Funcionário / Servidor Públicooptionalmunicipal / federal staffboth portariasverified
02/11Finadoscivil holidayeveryoneLei 662/1949verified
03/11(bridge)optionalmunicipal staffPortaria 2083/2025verified
04/11Aniversário de São Carlosreligious holidayeveryoneLei 10.257/1990verified
15/11Proclamação da Repúblicacivil holidayeveryoneLei 662/1949verified
20/11Consciência Negracivil holidayeveryoneLei 14.759/2023verified
24/12(Christmas Eve)optional; federal after 13:00municipal / federal staffboth portariasverified
25/12Natalcivil holidayeveryoneLei 662/1949verified
31/12(New Year's Eve)optional; federal after 13:00municipal / federal staffboth portariasverified

Still pending: the State of São Paulo's own optional days (a yearly decree for state staff) are not in the registry; Lei SP 9.497/1997 must be re-read when ALESP answers; every optional day of 2027 waits for its acts, as they are published in December.

The panel

CalendarPopover.qml opens on a hover of the time or the date, pins on a click, closes on Escape or a click outside: the bar's coordinator, unchanged. It is hung from the logo, so the time and the date open the same panel in the same place.

ViewHolds
The yearthe 12 months, the current one on a surface of its own; the upcoming events beside them
A monthone month at about 140 x 87 px a cell, the events named inside; the selected day's detail beside it

A month's name, or any day, focuses that month (the day selected); Year goes back; ‹ › step a year in the year view and a month in the month view, across the year's turn; Today returns to today. The year chosen stays until another is chosen, across closes. The detail lists every event of the day after the filters, each with kind, scope, for whom, basis, status (Verified · checked 18/10/2026, Calculated date, Pending its annual act), notes, and its sources: a click opens the official publication in the browser. A day whose events are all filtered out says how many hide.

Geometry from the screen, not its name. Four columns where the room is 1200 px or more, three below; a cell from the month's width, never under 26 px wide or 24 px high, the digits at 12 px; past the screen's height the body scrolls and the header, the filters and the legend stay. Measured on 18/10/2026 through IPC, no cursor moved:

ScreenPanelGridCells
ASUS, 2560 x 14401320 x 790 at x 620 (centered on 1280)4 x 3, the upcoming list besideabout 30 x 27 px
LG standing, 1080 x 1920960 x 15573 x 4, the upcoming list below, the legend wrappedabout 31 x 28 px

The grid

A day carries independent signals, each its own shape, never one more color:

SignalMeans
a filled chip in the scope's colora holiday for everyone
an outlined chipan optional day, or a holiday for a restricted group (Teachers' Day)
a teal dot under the numberan observance
a small corner dotmore than one event that day; the detail lists them
a ring around the whole celltoday
a neutral fillthe selected day

Scope colors are the bar's scopeColor(): red Brasil, blue São Paulo, mauve São Carlos, teal International. Today is a RING because a fill would collide: accent and blue are the same hex in Tokyo Night, so a filled today was pixel for pixel a São Paulo holiday. A day's main signal is its strongest event (a holiday for everyone, then a restricted one, then an optional day, then an observance); Carnaval's two records keep one outline and the corner dot.

The band's month card reads the same data through monthCells() with the default filters.

The keyboard

While the panel is PINNED (a click on the time or the date; a hover never takes the keyboard):

KeyYear viewFocused month
← →the day before / afterthe same, the month following the day
↑ ↓a week before / afterthe same
Page Up / Downthe year before / afterthe month before / after
Enter or Spacefocus the selected day's month
Hometodaytoday
Escapecloseback to the year

BarPanel offers each key to the panel first (keyPressed); one the panel leaves unaccepted falls through, Escape closing, so every other panel keeps its Escape. A day's step is computed at local noon, so it crosses months, years and 29/02 with no special case. Written on 18/10/2026 and not yet driven with real key presses: the test was stopped because a game was in fullscreen.

Filters

Two independent sets, Holidays.filters: WHERE (São Carlos, São Paulo, Brasil, International) and WHAT (holidays, optional days, observances). The defaults: the three places that hold in São Carlos, holidays and optional days; International and observances off, since the UN lists about 250 days. Reset appears when they differ. They drive the grid, the upcoming list and the detail at once, and last for the shell's session.

Offline, and what is computed when

The panel reads ~/.config/theme/calendar.json, which Nix built from the registry for 2024 to 2032; a year outside that range says it has no registry data. Nothing touches the network on open. The year's map is recomputed when the year, a filter or the file changes, never per frame; the day's turn moves today through the Bar's clock. After an edit to the registry, a rebuild writes the new file and the panel re-reads it on its next open. It re-reads on EVERY open on purpose: a rebuild swaps the symlink to a new store path, which the file watcher did not see on 18/10/2026, and a hot reload kept the old text, so the detail showed a source list from before the rebuild.

The source watch (GitHub Actions)

.github/workflows/holidays.yml, Wednesdays, and by hand. It fingerprints the TEXT of each source the registry cites (registry.py check-sources, curl and openssl, the runner's own) and compares it with registry/sources.lock.json:

  • unchanged: nothing happens;
  • changed, missing markers or unreachable: ONE issue, opened or commented, with the table. The registry is never edited: a page that changed may be a new act or a new menu, and only a person reading it can tell.

São Carlos' server omits its intermediate certificate: the fetch takes the issuer from the leaf's AIA field and verifies the chain with it, as a browser does, never skipping verification. Its portal page is fingerprinted only between Relação de feriados and Portarias:, so a new menu cannot raise an alarm, and losing the markers can. Planalto resets a connection for an unknown agent, so the agent is browser-shaped and still names the project.

On 1 December the same workflow opens the next year's audit checklist. Validation on every push is the flake check holiday-registry (nix flake check, the CI's quality gate). After a review, refresh the lock with registry.py check-sources registry --lock registry/sources.lock.json --update-lock.

Tests, and what was not

modules/home/desktop/calendar/scripts/test_registry.py, 20 tests, run by the flake check: the computus, Carnaval on 29/02/2028, a fixed 29/02 that never shifts, a range across the year's turn, the schema's refusals, every item of Portaria 2083/2025, the 14 general holidays, Good Friday's two sources, the two Corpus Christi records, the restricted Teachers' Day, a future optional day pending, Consciência Negra from 2024, observances only for the year read. Two consecutive source checks on 18/10/2026: nine unchanged, ALESP 503.

Seen on screen on 18/10/2026: the year view (ASUS and LG), April focused with 03/04's detail and sources, May with International and observances on. Not tried by hand: the hover and click paths themselves (the IPC opens the same panel), a source link opening, and the day's turn at midnight.

Checking it without a mouse

qs ipc call bar panel cal opens the year; qs ipc call bar calendar 2026-04-03 focuses a day; qs ipc call bar calendarFilter un flips a filter (reset restores them); qs ipc call bar panelOn cal DP-2 opens it on a named screen; qs ipc call bar close closes it.

How to

  • Add an event: a record in registry/holidays.json with its rule, place, kind, applicability, status, verified_at (the day you READ the source) and source ids; add the source to sources if new. Then python3 -I scripts/registry.py validate registry and the tests.
  • A new year's acts: add the year to confirmed of each recurring optional day the act lists (with its hours), and dated records for its bridge days. Extend Audit with the new act's items.
  • Fix a source: change its record, set checked_at and check to what really happened.
  • A divergence: record both sources and a notes; set disputed only when the answer itself changes. Never pick one in silence.
  • A divergence that changes the answer (two acts give different dates): disputed, both sources, and a note; the detail shows it in the warning color until a person settles it.
  • The UN list for a new year: download the page and run registry.py import-un page.html --year Y --checked YYYY-MM-DD > registry/observances-un.json.