feat: introduce meta-configs pattern and relocate fonts config

- Move fonts.toml from deployed location to meta/ directory
- Update install-fonts.ts to read from meta/ and support extras array
- Add comprehensive documentation explaining meta-configs pattern
- Add ZedMono NF font and update Zed editor keybindings/settings
This commit is contained in:
2026-01-02 18:38:35 -06:00
parent ed10dae8fa
commit 2cf33f7a24
7 changed files with 207 additions and 56 deletions
+98
View File
@@ -0,0 +1,98 @@
# Meta-Configs Pattern
This repository uses a **meta-config pattern** for configuration files that drive imperative actions during `chezmoi apply`, rather than being deployed directly to the filesystem.
## What are Meta-Configs?
Meta-configs are configuration files that:
1. **Are NOT deployed** to the target system
2. **Drive scripts** that perform imperative actions (downloads, installations, modifications)
3. **Live in `meta/`** at the repository root (outside `home/`)
4. **Trigger `run_onchange_*` scripts** when their content changes
This pattern separates "what to configure" from "configuration files that get deployed."
## Directory Structure
```
chezmoi/
├── meta/ # Meta-configs (NOT deployed)
│ ├── fonts.toml # Drives font installation
│ └── ... # Future meta-configs
├── home/ # Deployed to $HOME
│ ├── .config/
│ ├── .local/
│ └── ...
└── docs/
└── meta-configs.md # This file
```
## Current Meta-Configs
### `meta/fonts.toml`
Drives the `install-fonts.ts` script to download and install fonts.
```toml
[ui]
primary = "Inter"
fallback = "Noto Sans"
[mono]
primary = "Geist Mono"
fallback = "JetBrains Mono"
[extras]
# Fonts to install without category assignment
fonts = ["ZedMono NF"]
```
**Triggered by:** `run_onchange_after_install-fonts.sh.tmpl`
**Script:** `~/.local/bin/install-fonts.ts`
## How It Works
1. Edit a meta-config in `meta/` (e.g., `meta/fonts.toml`)
2. Run `chezmoi apply`
3. Chezmoi detects the file changed via `{{ include "../meta/fonts.toml" | sha256sum }}`
4. The corresponding `run_onchange_*` script executes
5. The script reads the meta-config and performs imperative actions
## Why This Pattern?
### Problem
Some configuration requires imperative actions:
- Downloading files from the internet
- Installing packages
- Modifying system state
These don't fit the declarative "deploy this file" model of chezmoi.
### Solution
Meta-configs provide:
- **Centralized configuration** - Easy to edit, version-controlled
- **Imperative execution** - Scripts perform the actual work
- **Change detection** - `run_onchange_*` only runs when config changes
- **Clear separation** - Meta-configs are clearly not "files to deploy"
## Adding a New Meta-Config
1. Create `meta/<name>.toml` with your configuration schema
2. Create a script in `home/dot_local/bin/` to process the config
3. Create `home/run_onchange_after_<name>.sh.tmpl` that:
- Includes a hash comment: `# hash: {{ include "../meta/<name>.toml" | sha256sum }}`
- Calls your processing script
4. Document the meta-config in this file
## Comparison with `.managed/`
| Aspect | `meta/` | `.managed/` |
| ------------ | ---------------------------------- | --------------------------------------- |
| Deployed | No | Yes (via symlinks) |
| Purpose | Drive imperative scripts | Source of truth for app configs |
| Consumed by | Chezmoi scripts | Applications (via symlinks) |
| Location | Repo root | Inside `home/` |
+22 -8
View File
@@ -10,24 +10,38 @@
"context": "Workspace", "context": "Workspace",
"bindings": { "bindings": {
// "shift shift": "file_finder::Toggle" // "shift shift": "file_finder::Toggle"
} },
}, },
{ {
"context": "Editor && vim_mode == insert", "context": "Editor && vim_mode == insert",
"bindings": { "bindings": {
// "j k": "vim::NormalBefore" "j k": "vim::NormalBefore",
} },
}, },
{ {
"context": "Editor", "context": "Editor",
"bindings": { "bindings": {
"alt-pageup": "editor::HalfPageUp" "alt-pageup": "editor::HalfPageUp",
} },
}, },
{ {
"context": "Editor", "context": "Editor",
"bindings": { "bindings": {
"alt-pagedown": "editor::HalfPageDown" "alt-pagedown": "editor::HalfPageDown",
} },
} },
{
"context": "Workspace",
"bindings": {
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePreviousItem",
},
},
{
"context": "Editor",
"bindings": {
"ctrl-alt-left": "pane::SwapItemLeft",
"ctrl-alt-right": "pane::SwapItemRight",
},
},
] ]
+10 -3
View File
@@ -7,12 +7,19 @@
// custom settings, run `zed: open default settings` from the // custom settings, run `zed: open default settings` from the
// command palette (cmd-shift-p / ctrl-shift-p) // command palette (cmd-shift-p / ctrl-shift-p)
{ {
"base_keymap": "Cursor", "vim_mode": false,
"icon_theme": "Material Icon Theme",
"use_system_path_prompts": false,
"base_keymap": "VSCode",
"ui_font_size": 16, "ui_font_size": 16,
"buffer_font_size": 15, "buffer_font_size": 15,
"auto_signature_help": false,
"theme": { "theme": {
"mode": "dark", "mode": "light",
"light": "One Light", "light": "One Light",
"dark": "Gruvbox Dark Hard" "dark": "Min Dark (Blurred)"
},
"terminal": {
"font_family": "ZedMono Nerd Font"
} }
} }
-31
View File
@@ -1,31 +0,0 @@
# Font Configuration for Chezmoi
# This file defines which fonts to install and configure.
# Fonts are sourced from Google Fonts automatically.
#
# To add a font: Just type its name - fuzzy matching will help if you misspell.
# To swap fonts: Change the primary, run `chezmoi apply`, done!
#
# Run `install-fonts.ts` manually to see available fonts or troubleshoot.
[ui]
# Sans-serif fonts for user interface elements
primary = "Inter"
fallback = "Noto Sans"
[serif]
# Serif fonts for documents and reading
primary = "Source Serif 4"
fallback = "Noto Serif"
[mono]
# Monospace fonts for code and terminals
primary = "Geist Mono"
fallback = "JetBrains Mono"
[emoji]
# Emoji font for unicode emoji support
primary = "Noto Color Emoji"
# Optional: Uncomment to install accessibility-focused fonts
# [accessibility]
# primary = "Atkinson Hyperlegible"
+40 -12
View File
@@ -49,11 +49,14 @@ interface FontDetails extends GoogleFont {
variants: FontVariant[]; variants: FontVariant[];
} }
interface FontCategoryConfig {
primary: string;
fallback?: string;
}
interface FontConfig { interface FontConfig {
[category: string]: { categories: Record<string, FontCategoryConfig>;
primary: string; extras: string[];
fallback?: string;
};
} }
interface FuseResult<T> { interface FuseResult<T> {
@@ -67,7 +70,8 @@ interface FuseResult<T> {
// ============================================================================ // ============================================================================
const FONTS_DIR = join(homedir(), ".local", "share", "fonts"); const FONTS_DIR = join(homedir(), ".local", "share", "fonts");
const CONFIG_PATH = join(homedir(), ".config", "fontconfig", "fonts.toml"); // Meta-config location: chezmoi source dir, not deployed to filesystem
const CONFIG_PATH = join(homedir(), ".local", "share", "chezmoi", "meta", "fonts.toml");
const API_BASE = "https://gwfh.mranftl.com/api"; const API_BASE = "https://gwfh.mranftl.com/api";
const CACHE_FILE = join(homedir(), ".cache", "font-catalog.json"); const CACHE_FILE = join(homedir(), ".cache", "font-catalog.json");
const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
@@ -81,6 +85,10 @@ interface GitHubFontConfig {
} }
const GITHUB_FONTS: Record<string, GitHubFontConfig> = { const GITHUB_FONTS: Record<string, GitHubFontConfig> = {
"ZedMono NF": {
repo: "ryanoasis/nerd-fonts",
assetPattern: /^ZedMono\.zip$/,
},
Iosevka: { Iosevka: {
repo: "be5invis/Iosevka", repo: "be5invis/Iosevka",
assetPattern: /^PkgTTF-Iosevka-[\d.]+\.zip$/, assetPattern: /^PkgTTF-Iosevka-[\d.]+\.zip$/,
@@ -517,7 +525,7 @@ async function loadConfig(): Promise<FontConfig> {
const content = await Bun.file(CONFIG_PATH).text(); const content = await Bun.file(CONFIG_PATH).text();
// Simple TOML parser for our specific format // Simple TOML parser for our specific format
const config: FontConfig = {}; const config: FontConfig = { categories: {}, extras: [] };
let currentSection = ""; let currentSection = "";
for (const line of content.split("\n")) { for (const line of content.split("\n")) {
@@ -530,16 +538,33 @@ async function loadConfig(): Promise<FontConfig> {
const sectionMatch = trimmed.match(/^\[(\w+)\]$/); const sectionMatch = trimmed.match(/^\[(\w+)\]$/);
if (sectionMatch) { if (sectionMatch) {
currentSection = sectionMatch[1]; currentSection = sectionMatch[1];
config[currentSection] = { primary: "" }; if (currentSection !== "extras") {
config.categories[currentSection] = { primary: "" };
}
continue; continue;
} }
// Key-value pair // Handle [extras] section - array of fonts
if (currentSection === "extras") {
// Match: fonts = ["Font1", "Font2"]
const arrayMatch = trimmed.match(/^fonts\s*=\s*\[(.*)\]$/);
if (arrayMatch) {
const arrayContent = arrayMatch[1];
// Parse quoted strings from array
const fontMatches = arrayContent.matchAll(/"([^"]+)"/g);
for (const match of fontMatches) {
config.extras.push(match[1]);
}
}
continue;
}
// Key-value pair for category sections
const kvMatch = trimmed.match(/^(\w+)\s*=\s*"([^"]+)"$/); const kvMatch = trimmed.match(/^(\w+)\s*=\s*"([^"]+)"$/);
if (kvMatch && currentSection) { if (kvMatch && currentSection && currentSection !== "extras") {
const [, key, value] = kvMatch; const [, key, value] = kvMatch;
if (key === "primary" || key === "fallback") { if (key === "primary" || key === "fallback") {
config[currentSection][key] = value; config.categories[currentSection][key] = value;
} }
} }
} }
@@ -630,13 +655,16 @@ async function installFromConfig(): Promise<void> {
mkdirSync(FONTS_DIR, { recursive: true }); mkdirSync(FONTS_DIR, { recursive: true });
} }
// Collect all fonts to install // Collect all fonts to install from categories
const fontsToInstall: string[] = []; const fontsToInstall: string[] = [];
for (const category of Object.values(config)) { for (const category of Object.values(config.categories)) {
if (category.primary) fontsToInstall.push(category.primary); if (category.primary) fontsToInstall.push(category.primary);
if (category.fallback) fontsToInstall.push(category.fallback); if (category.fallback) fontsToInstall.push(category.fallback);
} }
// Add extras
fontsToInstall.push(...config.extras);
// Remove duplicates // Remove duplicates
const uniqueFonts = [...new Set(fontsToInstall)]; const uniqueFonts = [...new Set(fontsToInstall)];
@@ -1,9 +1,9 @@
{{ if eq .chezmoi.os "linux" -}} {{ if eq .chezmoi.os "linux" -}}
#!/bin/bash #!/bin/bash
# Font Installer Hook # Font Installer Hook
# Runs automatically when fonts.toml changes # Runs automatically when meta/fonts.toml changes
# #
# fonts.toml hash: {{ include "dot_config/fontconfig/fonts.toml" | sha256sum }} # fonts.toml hash: {{ include "../meta/fonts.toml" | sha256sum }}
set -eu set -eu
+35
View File
@@ -0,0 +1,35 @@
# Font Installation Meta-Config
#
# This file drives the install-fonts.ts script.
# It is NOT deployed to the filesystem.
#
# Fonts are sourced from:
# - Google Fonts (via google-webfonts-helper API)
# - GitHub releases (for fonts like Iosevka, ZedMono NF)
#
# Run `install-fonts.ts --help` for manual usage.
# Run `install-fonts.ts --search <query>` to find fonts.
[ui]
# Sans-serif fonts for user interface elements
primary = "Inter"
fallback = "Noto Sans"
[serif]
# Serif fonts for documents and reading
primary = "Source Serif 4"
fallback = "Noto Serif"
[mono]
# Monospace fonts for code and terminals
primary = "Geist Mono"
fallback = "JetBrains Mono"
[emoji]
# Emoji font for unicode emoji support
primary = "Noto Color Emoji"
[extras]
# Additional fonts to install without category assignment
# Useful for fonts needed by specific applications
fonts = ["ZedMono NF"]