Some nice printable calendars to share.

updates README

+213 -148
+2
.gitignore
··· 1 1 dist/ 2 + fonts/ 2 3 .DS_Store 3 4 Thumbs.db 5 + *.pdf
+24
LICENSE
··· 1 + This is free and unencumbered software released into the public domain. 2 + 3 + Anyone is free to copy, modify, publish, use, compile, sell, or 4 + distribute this software, either in source code form or as a compiled 5 + binary, for any purpose, commercial or non-commercial, and by any 6 + means. 7 + 8 + In jurisdictions that recognize copyright laws, the author or authors 9 + of this software dedicate any and all copyright interest in the 10 + software to the public domain. We make this dedication for the benefit 11 + of the public at large and to the detriment of our heirs and 12 + successors. We intend this dedication to be an overt act of 13 + relinquishment in perpetuity of all present and future rights to this 14 + software under copyright law. 15 + 16 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 + IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 + OTHER DEALINGS IN THE SOFTWARE. 23 + 24 + For more information, please refer to <https://unlicense.org>
+36 -6
README.md
··· 3 3 I 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... 4 4 Then 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. 5 5 6 - ## How to Use 6 + ## Compilation 7 + 8 + ### Manual compilation 7 9 8 10 1. Install `typst` (if you haven't already). 9 - 2. Find a `.typ` file that strikes your fancy in this repository. 10 - 3. Open your terminal and run the compile command, for example: 11 + 2. Open your terminal and run the compile command, for example: 11 12 ```sh 12 - typst compile blue.typ 13 + typst compile --root calendars/blue.typ blue.pdf 13 14 ``` 14 - 4. Behold! A beautiful PDF of a calendar appears. 15 + 16 + ### Nix compilation 17 + 18 + ```sh 19 + # Init the development environment 20 + nix develop 21 + # Run the compile command 22 + typst compile calendars/blue.typ blue.pdf 23 + ``` 24 + 25 + ## Previews 26 + 27 + <table> 28 + <tr> 29 + <td align="center"> 30 + <img width="200" src="./previews/blue.png" /> 31 + <p>Blue, US Oficio sheet, 3 pages</p> 32 + </td> 33 + <td align="center"> 34 + <img width="400" src="./previews/large.png" /> 35 + <p>Large, 180x90cm, 1 page</p> 36 + </td> 37 + <td align="center"> 38 + <img width="200" src="./previews/horizontal.png" /> 39 + <p>Horizontal, US Letter sheet, 12 pages</p> 40 + </td> 41 + </tr> 42 + </table> 15 43 16 44 ## Font specimen 17 45 18 46 To 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. 47 + 48 + You can also put font files directly on the `./fonts` folders and will be included too. 19 49 20 50 ## License 21 51 22 - All the code in this repo is under CC0. Do as you please. 52 + [The Unlicense](./LICENSE). Do as you please.
+151 -142
flake.nix
··· 6 6 flake-parts.url = "github:hercules-ci/flake-parts"; 7 7 }; 8 8 9 - outputs = inputs@{ flake-parts, ... }: 10 - flake-parts.lib.mkFlake { inherit inputs; } { 11 - systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ]; 9 + outputs = inputs @ {flake-parts, ...}: 10 + flake-parts.lib.mkFlake {inherit inputs;} { 11 + systems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin"]; 12 12 13 - perSystem = { config, self', inputs', pkgs, system, ... }: 14 - let 15 - generateFontSpecimen = pkgs.writers.writePython3Bin "font-specimen" {} '' 16 - import subprocess 17 - import sys 18 - from datetime import datetime 19 - from collections import defaultdict 13 + perSystem = { 14 + config, 15 + self', 16 + inputs', 17 + pkgs, 18 + system, 19 + ... 20 + }: let 21 + generateFontSpecimen = pkgs.writers.writePython3Bin "font-specimen" {} '' 22 + import subprocess 23 + import sys 24 + from datetime import datetime 25 + from collections import defaultdict 20 26 21 27 22 - OUTPUT_TYP = "font-specimen.typ" 23 - OUTPUT_PDF = "font-specimen.pdf" 24 - PANGRAM = "The quick brown fox jumps over the lazy dog. 0123456789" 25 - ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz" 26 - DATE = datetime.now().strftime("%Y-%m-%d") 28 + OUTPUT_TYP = "font-specimen.typ" 29 + OUTPUT_PDF = "font-specimen.pdf" 30 + PANGRAM = "The quick brown fox jumps over the lazy dog. 0123456789" 31 + ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz" 32 + DATE = datetime.now().strftime("%Y-%m-%d") 27 33 28 - HEAVY_HITTERS = { 29 - "Noto", "SF", "Apple", "Hiragino", "STIX", "CJK", 30 - "DecoType", "FiraMono", "iMWritingMono" 31 - } 34 + HEAVY_HITTERS = { 35 + "Noto", "SF", "Apple", "Hiragino", "STIX", "CJK", 36 + "DecoType", "FiraMono", "iMWritingMono" 37 + } 32 38 33 39 34 - def is_likely_latin(font_name): 35 - # 1. Handle the Noto explosion: Only allow base Latin variants 36 - if font_name.startswith("Noto "): 37 - allowed = {"Noto Sans", "Noto Serif", "Noto Mono"} 38 - base = font_name.replace(" UI", "") 39 - if base not in allowed: 40 - return False 40 + def is_likely_latin(font_name): 41 + # 1. Handle the Noto explosion: Only allow base Latin variants 42 + if font_name.startswith("Noto "): 43 + allowed = {"Noto Sans", "Noto Serif", "Noto Mono"} 44 + base = font_name.replace(" UI", "") 45 + if base not in allowed: 46 + return False 47 + 48 + if font_name.startswith("STIX"): 49 + return False 41 50 42 - if font_name.startswith("STIX"): 43 - return False 51 + # 2. Block known non-Latin scripts, math, emoji, and macOS fallbacks 52 + blocked_terms = [ 53 + "Arabic", "Hebrew", "CJK", "Devanagari", "Bangla", "Gurmukhi", 54 + "Gujarati", "Oriya", "Tamil", "Telugu", "Kannada", "Malayalam", 55 + "Sinhala", "Thai", "Lao", "Khmer", "Myanmar", "Armenian", 56 + "Georgian", "Emoji", "Math", "Symbols", "Braille", "Gothic", 57 + "Mincho", "Songti", "Heiti", "Kufi", "Naskh", "Nastaliq", 58 + "Thuluth", "Ornaments", "Dingbats", "Webdings", "Wingdings", 59 + "Fallback", "LastResort", "Al Bayan", "Al Nile", "Al Tarikh", 60 + "Ayuthaya", "Baghdad", "Beirut", "Damascus", "DecoType", 61 + "Diwan", "Euphemia", "Farah", "Farisi", "Geeza", "Grantha", 62 + "Hiragino", "InaiMathi", "Kailasa", "Kefa", "Kohinoor", 63 + "Kokonor", "Krungthep", "Mishafi", "Muna", "Nadeem", 64 + "Peninim", "Plantagenet", "Raanana", "Sana", "Sathu", "Shree", 65 + "Silom", "Sukhumvit", "Waseem", "Zither" 66 + ] 44 67 45 - # 2. Block known non-Latin scripts, math, emoji, and macOS fallbacks 46 - blocked_terms = [ 47 - "Arabic", "Hebrew", "CJK", "Devanagari", "Bangla", "Gurmukhi", 48 - "Gujarati", "Oriya", "Tamil", "Telugu", "Kannada", "Malayalam", 49 - "Sinhala", "Thai", "Lao", "Khmer", "Myanmar", "Armenian", 50 - "Georgian", "Emoji", "Math", "Symbols", "Braille", "Gothic", 51 - "Mincho", "Songti", "Heiti", "Kufi", "Naskh", "Nastaliq", 52 - "Thuluth", "Ornaments", "Dingbats", "Webdings", "Wingdings", 53 - "Fallback", "LastResort", "Al Bayan", "Al Nile", "Al Tarikh", 54 - "Ayuthaya", "Baghdad", "Beirut", "Damascus", "DecoType", 55 - "Diwan", "Euphemia", "Farah", "Farisi", "Geeza", "Grantha", 56 - "Hiragino", "InaiMathi", "Kailasa", "Kefa", "Kohinoor", 57 - "Kokonor", "Krungthep", "Mishafi", "Muna", "Nadeem", 58 - "Peninim", "Plantagenet", "Raanana", "Sana", "Sathu", "Shree", 59 - "Silom", "Sukhumvit", "Waseem", "Zither" 60 - ] 68 + return not any(term in font_name for term in blocked_terms) 61 69 62 - return not any(term in font_name for term in blocked_terms) 63 70 71 + def get_fonts(): 72 + try: 73 + result = subprocess.run( 74 + ["typst", "fonts"], 75 + capture_output=True, 76 + text=True, 77 + check=True 78 + ) 79 + lines = result.stdout.splitlines() 80 + # Extract, clean, and immediately filter out non-Latin fonts 81 + fonts = [f.strip() for f in lines if f.strip()] 82 + return [f for f in fonts if is_likely_latin(f)] 83 + except FileNotFoundError: 84 + print("Error: 'typst' command not found.") 85 + sys.exit(1) 64 86 65 - def get_fonts(): 66 - try: 67 - result = subprocess.run( 68 - ["typst", "fonts"], 69 - capture_output=True, 70 - text=True, 71 - check=True 72 - ) 73 - lines = result.stdout.splitlines() 74 - # Extract, clean, and immediately filter out non-Latin fonts 75 - fonts = [f.strip() for f in lines if f.strip()] 76 - return [f for f in fonts if is_likely_latin(f)] 77 - except FileNotFoundError: 78 - print("Error: 'typst' command not found.") 79 - sys.exit(1) 80 87 88 + def deduplicate_fonts(fonts): 89 + grouped = defaultdict(list) 90 + for font in fonts: 91 + parts = font.split() 92 + if not parts: 93 + continue 94 + if parts[0] in HEAVY_HITTERS: 95 + prefix = parts[0] 96 + else: 97 + if len(parts) > 1: 98 + prefix = f"{parts[0]} {parts[1]}" 99 + else: 100 + prefix = parts[0] 101 + grouped[prefix].append(font) 81 102 82 - def deduplicate_fonts(fonts): 83 - grouped = defaultdict(list) 84 - for font in fonts: 85 - parts = font.split() 86 - if not parts: 87 - continue 88 - if parts[0] in HEAVY_HITTERS: 89 - prefix = parts[0] 90 - else: 91 - if len(parts) > 1: 92 - prefix = f"{parts[0]} {parts[1]}" 93 - else: 94 - prefix = parts[0] 95 - grouped[prefix].append(font) 103 + final_fonts = [] 104 + for prefix in sorted(grouped.keys()): 105 + family = grouped[prefix] 106 + if len(family) > 3: 107 + mid = len(family) // 2 108 + final_fonts.extend([family[0], family[mid], family[-1]]) 109 + else: 110 + final_fonts.extend(family) 111 + return sorted(list(set(final_fonts))) 96 112 97 - final_fonts = [] 98 - for prefix in sorted(grouped.keys()): 99 - family = grouped[prefix] 100 - if len(family) > 3: 101 - mid = len(family) // 2 102 - final_fonts.extend([family[0], family[mid], family[-1]]) 103 - else: 104 - final_fonts.extend(family) 105 - return sorted(list(set(final_fonts))) 106 113 114 + def generate_typst(fonts): 115 + fonts_str = "\n".join(f' "{f}",' for f in fonts) 116 + typst_content = f""" 117 + #set page(paper: "a4", margin: (x: 1cm, y: 1cm)) 118 + #set text(size: 10pt, font: "Arial") 107 119 108 - def generate_typst(fonts): 109 - fonts_str = "\n".join(f' "{f}",' for f in fonts) 110 - typst_content = f""" 111 - #set page(paper: "a4", margin: (x: 1cm, y: 1cm)) 112 - #set text(size: 10pt, font: "Arial") 120 + #align(center)[ 121 + #text(size: 26pt, weight: "bold")[Typst Font Specimen] \\ 122 + #v(0.2em) 123 + #text(size: 11pt, fill: luma(100))[Filtered & Grouped • Generated: {DATE}] 124 + ] 113 125 114 - #align(center)[ 115 - #text(size: 26pt, weight: "bold")[Typst Font Specimen] \\ 116 - #v(0.2em) 117 - #text(size: 11pt, fill: luma(100))[Filtered & Grouped • Generated: {DATE}] 118 - ] 126 + #v(1cm) 119 127 120 - #v(1cm) 128 + #let fonts = ( 129 + {fonts_str} 130 + ) 121 131 122 - #let fonts = ( 123 - {fonts_str} 124 - ) 132 + #grid( 133 + columns: (1fr, 1fr), 134 + column-gutter: 0.8cm, 135 + row-gutter: 1.2cm, 136 + ..fonts.map(f => {{ 137 + block(width: 100%, breakable: false)[ 138 + #text(weight: "bold", size: 9pt, fill: blue.darken(40%))[#f] \\ 139 + #v(0.3em) 140 + #text(font: f, size: 14pt, fallback: false)[{PANGRAM}] \\ 141 + #v(0.1em) 142 + #text(font: f, size: 10pt, fill: luma(80), fallback: false)[{ALPHABET}] 143 + #line(length: 100%, stroke: 0.2pt + luma(220)) 144 + ] 145 + }}) 146 + ) 147 + """ 148 + with open(OUTPUT_TYP, "w", encoding="utf-8") as f: 149 + f.write(typst_content.strip()) 125 150 126 - #grid( 127 - columns: (1fr, 1fr), 128 - column-gutter: 0.8cm, 129 - row-gutter: 1.2cm, 130 - ..fonts.map(f => {{ 131 - block(width: 100%, breakable: false)[ 132 - #text(weight: "bold", size: 9pt, fill: blue.darken(40%))[#f] \\ 133 - #v(0.3em) 134 - #text(font: f, size: 14pt, fallback: false)[{PANGRAM}] \\ 135 - #v(0.1em) 136 - #text(font: f, size: 10pt, fill: luma(80), fallback: false)[{ALPHABET}] 137 - #line(length: 100%, stroke: 0.2pt + luma(220)) 138 - ] 139 - }}) 140 - ) 141 - """ 142 - with open(OUTPUT_TYP, "w", encoding="utf-8") as f: 143 - f.write(typst_content.strip()) 151 + 152 + def main(): 153 + print("Retrieving and filtering font list...") 154 + fonts = get_fonts() 144 155 156 + print("Deduplicating families...") 157 + processed_fonts = deduplicate_fonts(fonts) 145 158 146 - def main(): 147 - print("Retrieving and filtering font list...") 148 - fonts = get_fonts() 159 + count = len(processed_fonts) 160 + print(f"Generating Typst source ({count} readable fonts mapped)...") 161 + generate_typst(processed_fonts) 149 162 150 - print("Deduplicating families...") 151 - processed_fonts = deduplicate_fonts(fonts) 163 + print("Compiling to PDF...") 164 + subprocess.run( 165 + ["typst", "compile", OUTPUT_TYP, OUTPUT_PDF], 166 + check=True 167 + ) 168 + print(f"Success! PDF generated at {OUTPUT_PDF}") 152 169 153 - count = len(processed_fonts) 154 - print(f"Generating Typst source ({count} readable fonts mapped)...") 155 - generate_typst(processed_fonts) 156 170 157 - print("Compiling to PDF...") 158 - subprocess.run( 159 - ["typst", "compile", OUTPUT_TYP, OUTPUT_PDF], 160 - check=True 161 - ) 162 - print(f"Success! PDF generated at {OUTPUT_PDF}") 171 + if __name__ == "__main__": 172 + main() 173 + ''; 174 + in { 175 + devShells.default = pkgs.mkShell { 176 + name = "typst-dev-shell"; 163 177 178 + TYPST_ROOT = "."; 179 + TYPST_FONT_PATHS = "./fonts"; 164 180 165 - if __name__ == "__main__": 166 - main() 167 - ''; 168 - in { 169 - devShells.default = pkgs.mkShell { 170 - name = "typst-dev-shell"; 171 - packages = with pkgs; [ 172 - typst 173 - tinymist 174 - opencode 175 - generateFontSpecimen 176 - ]; 177 - }; 181 + packages = with pkgs; [ 182 + typst 183 + tinymist 184 + generateFontSpecimen 185 + ]; 178 186 }; 187 + }; 179 188 }; 180 189 }
previews/blue.png

This is a binary file and will not be displayed.

previews/horizontal.png

This is a binary file and will not be displayed.

previews/large.png

This is a binary file and will not be displayed.