···11+This is free and unencumbered software released into the public domain.
22+33+Anyone is free to copy, modify, publish, use, compile, sell, or
44+distribute this software, either in source code form or as a compiled
55+binary, for any purpose, commercial or non-commercial, and by any
66+means.
77+88+In jurisdictions that recognize copyright laws, the author or authors
99+of this software dedicate any and all copyright interest in the
1010+software to the public domain. We make this dedication for the benefit
1111+of the public at large and to the detriment of our heirs and
1212+successors. We intend this dedication to be an overt act of
1313+relinquishment in perpetuity of all present and future rights to this
1414+software under copyright law.
1515+1616+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1717+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1818+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
1919+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
2020+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
2121+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
2222+OTHER DEALINGS IN THE SOFTWARE.
2323+2424+For more information, please refer to <https://unlicense.org>
+36-6
README.md
···33I used to use these free calendars some stores gift you, or the typical blue grid you can buy in the street. But I need to make small annotations some days to keep track of things, and these never had space...
44Then I found about [typst](https://typst.app), which intends to be the next LaTeX. And it's actually pretty good. So if the wind of inspiration blows new calendars might appear. Or not. Contributions and ideas are welcome though.
5566-## How to Use
66+## Compilation
77+88+### Manual compilation
798101. Install `typst` (if you haven't already).
99-2. Find a `.typ` file that strikes your fancy in this repository.
1010-3. Open your terminal and run the compile command, for example:
1111+2. Open your terminal and run the compile command, for example:
1112 ```sh
1212- typst compile blue.typ
1313+ typst compile --root calendars/blue.typ blue.pdf
1314 ```
1414-4. Behold! A beautiful PDF of a calendar appears.
1515+1616+### Nix compilation
1717+1818+```sh
1919+# Init the development environment
2020+nix develop
2121+# Run the compile command
2222+typst compile calendars/blue.typ blue.pdf
2323+```
2424+2525+## Previews
2626+2727+<table>
2828+ <tr>
2929+ <td align="center">
3030+ <img width="200" src="./previews/blue.png" />
3131+ <p>Blue, US Oficio sheet, 3 pages</p>
3232+ </td>
3333+ <td align="center">
3434+ <img width="400" src="./previews/large.png" />
3535+ <p>Large, 180x90cm, 1 page</p>
3636+ </td>
3737+ <td align="center">
3838+ <img width="200" src="./previews/horizontal.png" />
3939+ <p>Horizontal, US Letter sheet, 12 pages</p>
4040+ </td>
4141+ </tr>
4242+</table>
15431644## Font specimen
17451846To help find suitable fonts among the ones installed in your system, the dev environment includes the script `font-specimen` which generates a typst document and its compiled PDF, with a sample for each of the fonts available to typst. Feel free to modify the document and recompile it.
4747+4848+You can also put font files directly on the `./fonts` folders and will be included too.
19492050## License
21512222-All the code in this repo is under CC0. Do as you please.
5252+[The Unlicense](./LICENSE). Do as you please.
+151-142
flake.nix
···66 flake-parts.url = "github:hercules-ci/flake-parts";
77 };
8899- outputs = inputs@{ flake-parts, ... }:
1010- flake-parts.lib.mkFlake { inherit inputs; } {
1111- systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
99+ outputs = inputs @ {flake-parts, ...}:
1010+ flake-parts.lib.mkFlake {inherit inputs;} {
1111+ systems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin"];
12121313- perSystem = { config, self', inputs', pkgs, system, ... }:
1414- let
1515- generateFontSpecimen = pkgs.writers.writePython3Bin "font-specimen" {} ''
1616- import subprocess
1717- import sys
1818- from datetime import datetime
1919- from collections import defaultdict
1313+ perSystem = {
1414+ config,
1515+ self',
1616+ inputs',
1717+ pkgs,
1818+ system,
1919+ ...
2020+ }: let
2121+ generateFontSpecimen = pkgs.writers.writePython3Bin "font-specimen" {} ''
2222+ import subprocess
2323+ import sys
2424+ from datetime import datetime
2525+ from collections import defaultdict
202621272222- OUTPUT_TYP = "font-specimen.typ"
2323- OUTPUT_PDF = "font-specimen.pdf"
2424- PANGRAM = "The quick brown fox jumps over the lazy dog. 0123456789"
2525- ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz"
2626- DATE = datetime.now().strftime("%Y-%m-%d")
2828+ OUTPUT_TYP = "font-specimen.typ"
2929+ OUTPUT_PDF = "font-specimen.pdf"
3030+ PANGRAM = "The quick brown fox jumps over the lazy dog. 0123456789"
3131+ ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz"
3232+ DATE = datetime.now().strftime("%Y-%m-%d")
27332828- HEAVY_HITTERS = {
2929- "Noto", "SF", "Apple", "Hiragino", "STIX", "CJK",
3030- "DecoType", "FiraMono", "iMWritingMono"
3131- }
3434+ HEAVY_HITTERS = {
3535+ "Noto", "SF", "Apple", "Hiragino", "STIX", "CJK",
3636+ "DecoType", "FiraMono", "iMWritingMono"
3737+ }
323833393434- def is_likely_latin(font_name):
3535- # 1. Handle the Noto explosion: Only allow base Latin variants
3636- if font_name.startswith("Noto "):
3737- allowed = {"Noto Sans", "Noto Serif", "Noto Mono"}
3838- base = font_name.replace(" UI", "")
3939- if base not in allowed:
4040- return False
4040+ def is_likely_latin(font_name):
4141+ # 1. Handle the Noto explosion: Only allow base Latin variants
4242+ if font_name.startswith("Noto "):
4343+ allowed = {"Noto Sans", "Noto Serif", "Noto Mono"}
4444+ base = font_name.replace(" UI", "")
4545+ if base not in allowed:
4646+ return False
4747+4848+ if font_name.startswith("STIX"):
4949+ return False
41504242- if font_name.startswith("STIX"):
4343- return False
5151+ # 2. Block known non-Latin scripts, math, emoji, and macOS fallbacks
5252+ blocked_terms = [
5353+ "Arabic", "Hebrew", "CJK", "Devanagari", "Bangla", "Gurmukhi",
5454+ "Gujarati", "Oriya", "Tamil", "Telugu", "Kannada", "Malayalam",
5555+ "Sinhala", "Thai", "Lao", "Khmer", "Myanmar", "Armenian",
5656+ "Georgian", "Emoji", "Math", "Symbols", "Braille", "Gothic",
5757+ "Mincho", "Songti", "Heiti", "Kufi", "Naskh", "Nastaliq",
5858+ "Thuluth", "Ornaments", "Dingbats", "Webdings", "Wingdings",
5959+ "Fallback", "LastResort", "Al Bayan", "Al Nile", "Al Tarikh",
6060+ "Ayuthaya", "Baghdad", "Beirut", "Damascus", "DecoType",
6161+ "Diwan", "Euphemia", "Farah", "Farisi", "Geeza", "Grantha",
6262+ "Hiragino", "InaiMathi", "Kailasa", "Kefa", "Kohinoor",
6363+ "Kokonor", "Krungthep", "Mishafi", "Muna", "Nadeem",
6464+ "Peninim", "Plantagenet", "Raanana", "Sana", "Sathu", "Shree",
6565+ "Silom", "Sukhumvit", "Waseem", "Zither"
6666+ ]
44674545- # 2. Block known non-Latin scripts, math, emoji, and macOS fallbacks
4646- blocked_terms = [
4747- "Arabic", "Hebrew", "CJK", "Devanagari", "Bangla", "Gurmukhi",
4848- "Gujarati", "Oriya", "Tamil", "Telugu", "Kannada", "Malayalam",
4949- "Sinhala", "Thai", "Lao", "Khmer", "Myanmar", "Armenian",
5050- "Georgian", "Emoji", "Math", "Symbols", "Braille", "Gothic",
5151- "Mincho", "Songti", "Heiti", "Kufi", "Naskh", "Nastaliq",
5252- "Thuluth", "Ornaments", "Dingbats", "Webdings", "Wingdings",
5353- "Fallback", "LastResort", "Al Bayan", "Al Nile", "Al Tarikh",
5454- "Ayuthaya", "Baghdad", "Beirut", "Damascus", "DecoType",
5555- "Diwan", "Euphemia", "Farah", "Farisi", "Geeza", "Grantha",
5656- "Hiragino", "InaiMathi", "Kailasa", "Kefa", "Kohinoor",
5757- "Kokonor", "Krungthep", "Mishafi", "Muna", "Nadeem",
5858- "Peninim", "Plantagenet", "Raanana", "Sana", "Sathu", "Shree",
5959- "Silom", "Sukhumvit", "Waseem", "Zither"
6060- ]
6868+ return not any(term in font_name for term in blocked_terms)
61696262- return not any(term in font_name for term in blocked_terms)
63707171+ def get_fonts():
7272+ try:
7373+ result = subprocess.run(
7474+ ["typst", "fonts"],
7575+ capture_output=True,
7676+ text=True,
7777+ check=True
7878+ )
7979+ lines = result.stdout.splitlines()
8080+ # Extract, clean, and immediately filter out non-Latin fonts
8181+ fonts = [f.strip() for f in lines if f.strip()]
8282+ return [f for f in fonts if is_likely_latin(f)]
8383+ except FileNotFoundError:
8484+ print("Error: 'typst' command not found.")
8585+ sys.exit(1)
64866565- def get_fonts():
6666- try:
6767- result = subprocess.run(
6868- ["typst", "fonts"],
6969- capture_output=True,
7070- text=True,
7171- check=True
7272- )
7373- lines = result.stdout.splitlines()
7474- # Extract, clean, and immediately filter out non-Latin fonts
7575- fonts = [f.strip() for f in lines if f.strip()]
7676- return [f for f in fonts if is_likely_latin(f)]
7777- except FileNotFoundError:
7878- print("Error: 'typst' command not found.")
7979- sys.exit(1)
80878888+ def deduplicate_fonts(fonts):
8989+ grouped = defaultdict(list)
9090+ for font in fonts:
9191+ parts = font.split()
9292+ if not parts:
9393+ continue
9494+ if parts[0] in HEAVY_HITTERS:
9595+ prefix = parts[0]
9696+ else:
9797+ if len(parts) > 1:
9898+ prefix = f"{parts[0]} {parts[1]}"
9999+ else:
100100+ prefix = parts[0]
101101+ grouped[prefix].append(font)
811028282- def deduplicate_fonts(fonts):
8383- grouped = defaultdict(list)
8484- for font in fonts:
8585- parts = font.split()
8686- if not parts:
8787- continue
8888- if parts[0] in HEAVY_HITTERS:
8989- prefix = parts[0]
9090- else:
9191- if len(parts) > 1:
9292- prefix = f"{parts[0]} {parts[1]}"
9393- else:
9494- prefix = parts[0]
9595- grouped[prefix].append(font)
103103+ final_fonts = []
104104+ for prefix in sorted(grouped.keys()):
105105+ family = grouped[prefix]
106106+ if len(family) > 3:
107107+ mid = len(family) // 2
108108+ final_fonts.extend([family[0], family[mid], family[-1]])
109109+ else:
110110+ final_fonts.extend(family)
111111+ return sorted(list(set(final_fonts)))
961129797- final_fonts = []
9898- for prefix in sorted(grouped.keys()):
9999- family = grouped[prefix]
100100- if len(family) > 3:
101101- mid = len(family) // 2
102102- final_fonts.extend([family[0], family[mid], family[-1]])
103103- else:
104104- final_fonts.extend(family)
105105- return sorted(list(set(final_fonts)))
106113114114+ def generate_typst(fonts):
115115+ fonts_str = "\n".join(f' "{f}",' for f in fonts)
116116+ typst_content = f"""
117117+ #set page(paper: "a4", margin: (x: 1cm, y: 1cm))
118118+ #set text(size: 10pt, font: "Arial")
107119108108- def generate_typst(fonts):
109109- fonts_str = "\n".join(f' "{f}",' for f in fonts)
110110- typst_content = f"""
111111- #set page(paper: "a4", margin: (x: 1cm, y: 1cm))
112112- #set text(size: 10pt, font: "Arial")
120120+ #align(center)[
121121+ #text(size: 26pt, weight: "bold")[Typst Font Specimen] \\
122122+ #v(0.2em)
123123+ #text(size: 11pt, fill: luma(100))[Filtered & Grouped • Generated: {DATE}]
124124+ ]
113125114114- #align(center)[
115115- #text(size: 26pt, weight: "bold")[Typst Font Specimen] \\
116116- #v(0.2em)
117117- #text(size: 11pt, fill: luma(100))[Filtered & Grouped • Generated: {DATE}]
118118- ]
126126+ #v(1cm)
119127120120- #v(1cm)
128128+ #let fonts = (
129129+ {fonts_str}
130130+ )
121131122122- #let fonts = (
123123- {fonts_str}
124124- )
132132+ #grid(
133133+ columns: (1fr, 1fr),
134134+ column-gutter: 0.8cm,
135135+ row-gutter: 1.2cm,
136136+ ..fonts.map(f => {{
137137+ block(width: 100%, breakable: false)[
138138+ #text(weight: "bold", size: 9pt, fill: blue.darken(40%))[#f] \\
139139+ #v(0.3em)
140140+ #text(font: f, size: 14pt, fallback: false)[{PANGRAM}] \\
141141+ #v(0.1em)
142142+ #text(font: f, size: 10pt, fill: luma(80), fallback: false)[{ALPHABET}]
143143+ #line(length: 100%, stroke: 0.2pt + luma(220))
144144+ ]
145145+ }})
146146+ )
147147+ """
148148+ with open(OUTPUT_TYP, "w", encoding="utf-8") as f:
149149+ f.write(typst_content.strip())
125150126126- #grid(
127127- columns: (1fr, 1fr),
128128- column-gutter: 0.8cm,
129129- row-gutter: 1.2cm,
130130- ..fonts.map(f => {{
131131- block(width: 100%, breakable: false)[
132132- #text(weight: "bold", size: 9pt, fill: blue.darken(40%))[#f] \\
133133- #v(0.3em)
134134- #text(font: f, size: 14pt, fallback: false)[{PANGRAM}] \\
135135- #v(0.1em)
136136- #text(font: f, size: 10pt, fill: luma(80), fallback: false)[{ALPHABET}]
137137- #line(length: 100%, stroke: 0.2pt + luma(220))
138138- ]
139139- }})
140140- )
141141- """
142142- with open(OUTPUT_TYP, "w", encoding="utf-8") as f:
143143- f.write(typst_content.strip())
151151+152152+ def main():
153153+ print("Retrieving and filtering font list...")
154154+ fonts = get_fonts()
144155156156+ print("Deduplicating families...")
157157+ processed_fonts = deduplicate_fonts(fonts)
145158146146- def main():
147147- print("Retrieving and filtering font list...")
148148- fonts = get_fonts()
159159+ count = len(processed_fonts)
160160+ print(f"Generating Typst source ({count} readable fonts mapped)...")
161161+ generate_typst(processed_fonts)
149162150150- print("Deduplicating families...")
151151- processed_fonts = deduplicate_fonts(fonts)
163163+ print("Compiling to PDF...")
164164+ subprocess.run(
165165+ ["typst", "compile", OUTPUT_TYP, OUTPUT_PDF],
166166+ check=True
167167+ )
168168+ print(f"Success! PDF generated at {OUTPUT_PDF}")
152169153153- count = len(processed_fonts)
154154- print(f"Generating Typst source ({count} readable fonts mapped)...")
155155- generate_typst(processed_fonts)
156170157157- print("Compiling to PDF...")
158158- subprocess.run(
159159- ["typst", "compile", OUTPUT_TYP, OUTPUT_PDF],
160160- check=True
161161- )
162162- print(f"Success! PDF generated at {OUTPUT_PDF}")
171171+ if __name__ == "__main__":
172172+ main()
173173+ '';
174174+ in {
175175+ devShells.default = pkgs.mkShell {
176176+ name = "typst-dev-shell";
163177178178+ TYPST_ROOT = ".";
179179+ TYPST_FONT_PATHS = "./fonts";
164180165165- if __name__ == "__main__":
166166- main()
167167- '';
168168- in {
169169- devShells.default = pkgs.mkShell {
170170- name = "typst-dev-shell";
171171- packages = with pkgs; [
172172- typst
173173- tinymist
174174- opencode
175175- generateFontSpecimen
176176- ];
177177- };
181181+ packages = with pkgs; [
182182+ typst
183183+ tinymist
184184+ generateFontSpecimen
185185+ ];
178186 };
187187+ };
179188 };
180189}