Skip to content

Commit ca167e7

Browse files
committed
setup formatter
1 parent c373590 commit ca167e7

8 files changed

Lines changed: 159 additions & 36 deletions

File tree

.github/workflows/build.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ jobs:
2020
steps:
2121
- uses: actions/checkout@v5
2222
- uses: nixbuild/nix-quick-install-action@v35
23+
# Includes the treefmt `formatting` check (nix/python/shell/yaml), so a
24+
# dirty format fails here — no separate `nix fmt && git diff` step needed.
2325
- run: nix flake check --all-systems
24-
- run: nix fmt . && git diff --exit-code
2526

2627
build:
2728
name: Build ${{ matrix.system }}

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# computer
44

55
nix files for:
6+
67
- my mac
78
- my remote [exe.dev](https://exe.dev) machine
89

@@ -93,12 +94,12 @@ nodes; use an ephemeral key so retired ones auto-clean (see step 3).
9394
statedir (not backed up), so a fresh machine registers new nodes; the
9495
ephemeral key lets retired ones auto-remove once offline — no manual cleanup.
9596
96-
4. Enable **HTTPS Certificates** (admin console → DNS → *Enable HTTPS*, needs
97+
4. Enable **HTTPS Certificates** (admin console → DNS → _Enable HTTPS_, needs
9798
MagicDNS on). Required to provision the `*.ts.net` certs. The `computer`
9899
node's `tailscale-serve` service re-asserts this on every boot:
99100
- `https://computer.<tailnet>.ts.net/<tenant>/` → ingress, **public via
100101
Funnel** (needs the `nodeAttrs` above). Unauthenticated — see the note in
101-
`hosts/exedev/default.nix`. This one is named after the *node*, so on a
102+
`hosts/exedev/default.nix`. This one is named after the _node_, so on a
102103
recreation where the retired ephemeral node hasn't dropped yet Tailscale
103104
may suffix it (`computer-1`) until the stale one is culled; exe.dev's
104105
public share is the stable public path if that matters.
@@ -161,9 +162,9 @@ nodes; use an ephemeral key so retired ones auto-clean (see step 3).
161162
162163
We have to store it outside of the machine to be able to restore everything else on startup.
163164
164-
| Variable | Description |
165-
| --- | --- |
165+
| Variable | Description |
166+
| ------------------- | ---------------------------------------- |
166167
| `RESTIC_REPOSITORY` | B2 restic repo, e.g. `b2:backups:exedev` |
167-
| `RESTIC_PASSWORD` | restic repo encryption password |
168-
| `B2_ACCOUNT_ID` | B2 key id |
169-
| `B2_ACCOUNT_KEY` | B2 application key (scope to the bucket) |
168+
| `RESTIC_PASSWORD` | restic repo encryption password |
169+
| `B2_ACCOUNT_ID` | B2 key id |
170+
| `B2_ACCOUNT_KEY` | B2 application key (scope to the bucket) |

flake.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737
# nix-darwin `homebrew` module only manages an already-installed brew.
3838
# (No nixpkgs input to follow; it pins Homebrew/brew via its own brew-src.)
3939
nix-homebrew.url = "github:zhaofengli/nix-homebrew";
40+
# One `nix fmt` / `nix flake check` gate over the whole tree (nix, python,
41+
# shell, yaml) instead of nix-only. Config lives in ./treefmt.nix.
42+
treefmt-nix = {
43+
url = "github:numtide/treefmt-nix";
44+
inputs.nixpkgs.follows = "nixpkgs";
45+
};
4046
};
4147

4248
outputs =
@@ -70,6 +76,9 @@
7076
./hosts/exedev;
7177

7278
releaseFor = system: import ./packages/release { pkgs = nixpkgs.legacyPackages.${system}; };
79+
80+
treefmtFor =
81+
system: inputs.treefmt-nix.lib.evalModule nixpkgs.legacyPackages.${system} ./treefmt.nix;
7382
in
7483
{
7584
# This Mac, configured with a Linux builder VM (so it can build *-linux).
@@ -110,6 +119,12 @@
110119
};
111120
});
112121

113-
formatter = lib.genAttrs allSystems (system: nixpkgs.legacyPackages.${system}.nixfmt-tree);
122+
formatter = lib.genAttrs allSystems (system: (treefmtFor system).config.build.wrapper);
123+
124+
# `nix flake check` fails if the tree isn't treefmt-clean. CI relies on this
125+
# instead of a bespoke `nix fmt && git diff` step.
126+
checks = lib.genAttrs allSystems (system: {
127+
formatting = (treefmtFor system).config.build.check self;
128+
});
114129
};
115130
}

packages/vault-sync/discogs.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ def sync_release(item, references, attachments, token, lp_date, index):
9090
url = release_url(info)
9191
title = info["title"]
9292
year = info.get("year") or ""
93-
artists = [clean_artist(a["name"]) for a in info.get("artists", []) if a.get("name")]
93+
artists = [
94+
clean_artist(a["name"]) for a in info.get("artists", []) if a.get("name")
95+
]
9496

9597
existing = index.get(url.rstrip("/"))
9698
if existing:
@@ -107,16 +109,21 @@ def sync_release(item, references, attachments, token, lp_date, index):
107109
ext = os.path.splitext(cover.split("?")[0])[1] or ".jpeg"
108110
cover_file = f"{base}{ext}"
109111
try:
110-
vaultlib.download(cover, os.path.join(attachments, cover_file),
111-
{"User-Agent": UA, "Authorization": f"Discogs token={token}"})
112+
vaultlib.download(
113+
cover,
114+
os.path.join(attachments, cover_file),
115+
{"User-Agent": UA, "Authorization": f"Discogs token={token}"},
116+
)
112117
except Exception as e:
113118
print(f" war: cover download failed for {title}: {e}", file=sys.stderr)
114119
cover_file = None
115120

116121
for a in artists:
117122
vaultlib.ensure_person_note(references, a, "Artists")
118123

119-
vaultlib.write_note(path, album_note(title, year, artists, cover_file, url, lp_date))
124+
vaultlib.write_note(
125+
path, album_note(title, year, artists, cover_file, url, lp_date)
126+
)
120127
index[url.rstrip("/")] = path
121128
kind = "owned" if lp_date else "wishlist"
122129
print(f" new album ({kind}): {os.path.basename(path)} ({', '.join(artists)})")
@@ -132,10 +139,14 @@ def main(username, token, vault, include_wantlist):
132139
index = vaultlib.index_by_field(references, "discogs")
133140
created = updated = 0
134141

135-
collection = urljoin(BASE_URL, f"/users/{username}/collection/folders/0/releases?sort=artist")
142+
collection = urljoin(
143+
BASE_URL, f"/users/{username}/collection/folders/0/releases?sort=artist"
144+
)
136145
for item in paginate(collection, token, "releases"):
137146
date_added = (item.get("date_added") or "")[:10] # ISO ts -> YYYY-MM-DD
138-
result = sync_release(item, references, attachments, token, date_added or None, index)
147+
result = sync_release(
148+
item, references, attachments, token, date_added or None, index
149+
)
139150
created += result == "new"
140151
updated += result == "updated"
141152

@@ -149,11 +160,23 @@ def main(username, token, vault, include_wantlist):
149160

150161

151162
if __name__ == "__main__":
152-
p = argparse.ArgumentParser(description="Sync Discogs collection + wantlist into vault Album notes.")
153-
p.add_argument("-u", "--username", default=os.environ.get("DISCOGS_USERNAME", "ngalaiko"))
163+
p = argparse.ArgumentParser(
164+
description="Sync Discogs collection + wantlist into vault Album notes."
165+
)
166+
p.add_argument(
167+
"-u", "--username", default=os.environ.get("DISCOGS_USERNAME", "ngalaiko")
168+
)
154169
p.add_argument("-t", "--token", default=os.environ.get("DISCOGS_TOKEN"))
155-
p.add_argument("--vault", default=os.environ.get("OBSIDIAN_VAULT_DIR", "/var/lib/assistant/Vault"))
156-
p.add_argument("--no-wantlist", dest="wantlist", action="store_false", help="skip the wantlist (owned records only)")
170+
p.add_argument(
171+
"--vault",
172+
default=os.environ.get("OBSIDIAN_VAULT_DIR", "/var/lib/assistant/Vault"),
173+
)
174+
p.add_argument(
175+
"--no-wantlist",
176+
dest="wantlist",
177+
action="store_false",
178+
help="skip the wantlist (owned records only)",
179+
)
157180
args = p.parse_args()
158181
if not args.token:
159182
sys.exit("discogs: no token (set DISCOGS_TOKEN or pass --token)")

packages/vault-sync/letterboxd.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ def parse_feed(body):
5555
"url": f"https://letterboxd.com/film/{slug_match.group(1)}/",
5656
"year": year_el.text if year_el is not None else "",
5757
"watched": watched_el.text, # YYYY-MM-DD
58-
"liked": item.findtext("letterboxd:memberLike", default="No", namespaces=NS) == "Yes",
58+
"liked": item.findtext("letterboxd:memberLike", default="No", namespaces=NS)
59+
== "Yes",
5960
"poster": poster,
6061
}
6162

@@ -67,11 +68,15 @@ def scrape_directors(film_url):
6768
except Exception as e: # network hiccup — create the note without directors
6869
print(f" war: could not fetch {film_url}: {e}", file=sys.stderr)
6970
return []
70-
m = re.search(r'<script type="application/ld\+json">(.*?)</script>', html, re.DOTALL)
71+
m = re.search(
72+
r'<script type="application/ld\+json">(.*?)</script>', html, re.DOTALL
73+
)
7174
if not m:
7275
return []
7376
blob = m.group(1)
74-
blob = re.sub(r"/\*.*?\*/", "", blob, flags=re.DOTALL).strip() # strip CDATA comments
77+
blob = re.sub(
78+
r"/\*.*?\*/", "", blob, flags=re.DOTALL
79+
).strip() # strip CDATA comments
7580
try:
7681
data = json.loads(blob)
7782
except json.JSONDecodeError:
@@ -115,16 +120,23 @@ def main(username, vault):
115120
print(f" + watched {entry['watched']}: {os.path.basename(existing)}")
116121
continue
117122

118-
path, base = vaultlib.unique_note_path(references, entry["title"], entry["year"])
123+
path, base = vaultlib.unique_note_path(
124+
references, entry["title"], entry["year"]
125+
)
119126

120127
poster_file = None
121128
if entry["poster"]:
122129
ext = os.path.splitext(entry["poster"].split("?")[0])[1] or ".jpg"
123130
poster_file = f"{base}{ext}"
124131
try:
125-
vaultlib.download(entry["poster"], os.path.join(attachments, poster_file), HEADERS)
132+
vaultlib.download(
133+
entry["poster"], os.path.join(attachments, poster_file), HEADERS
134+
)
126135
except Exception as e:
127-
print(f" war: poster download failed for {entry['title']}: {e}", file=sys.stderr)
136+
print(
137+
f" war: poster download failed for {entry['title']}: {e}",
138+
file=sys.stderr,
139+
)
128140
poster_file = None
129141

130142
directors = scrape_directors(entry["url"])
@@ -133,18 +145,34 @@ def main(username, vault):
133145

134146
vaultlib.write_note(
135147
path,
136-
movie_note(entry["title"], entry["year"], poster_file, directors,
137-
entry["url"], entry["liked"], entry["watched"]),
148+
movie_note(
149+
entry["title"],
150+
entry["year"],
151+
poster_file,
152+
directors,
153+
entry["url"],
154+
entry["liked"],
155+
entry["watched"],
156+
),
138157
)
139158
created += 1
140-
print(f" new movie: {os.path.basename(path)} (dir: {', '.join(directors) or '?'})")
159+
print(
160+
f" new movie: {os.path.basename(path)} (dir: {', '.join(directors) or '?'})"
161+
)
141162

142163
print(f"letterboxd: {created} new, {updated} updated")
143164

144165

145166
if __name__ == "__main__":
146-
p = argparse.ArgumentParser(description="Sync Letterboxd diary into vault Movie notes.")
147-
p.add_argument("-u", "--username", default=os.environ.get("LETTERBOXD_USERNAME", "ngalaiko"))
148-
p.add_argument("--vault", default=os.environ.get("OBSIDIAN_VAULT_DIR", "/var/lib/assistant/Vault"))
167+
p = argparse.ArgumentParser(
168+
description="Sync Letterboxd diary into vault Movie notes."
169+
)
170+
p.add_argument(
171+
"-u", "--username", default=os.environ.get("LETTERBOXD_USERNAME", "ngalaiko")
172+
)
173+
p.add_argument(
174+
"--vault",
175+
default=os.environ.get("OBSIDIAN_VAULT_DIR", "/var/lib/assistant/Vault"),
176+
)
149177
args = p.parse_args()
150178
main(args.username, args.vault)

packages/vault-sync/vaultlib.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def index_by_field(references_dir, key):
7070
continue
7171
for line in lines:
7272
if line.startswith(needle):
73-
index[line[len(needle):].strip().rstrip("/")] = path
73+
index[line[len(needle) :].strip().rstrip("/")] = path
7474
break
7575
return index
7676

@@ -99,14 +99,23 @@ def add_list_item(path, key, item):
9999

100100
if not lines or lines[0].rstrip("\n") != "---":
101101
return False
102-
fm_end = next((i for i in range(1, len(lines)) if lines[i].rstrip("\n") == "---"), None)
102+
fm_end = next(
103+
(i for i in range(1, len(lines)) if lines[i].rstrip("\n") == "---"), None
104+
)
103105
if fm_end is None:
104106
return False
105-
key_idx = next((i for i in range(1, fm_end) if re.match(rf"^{re.escape(key)}:( |$)", lines[i].rstrip("\n"))), None)
107+
key_idx = next(
108+
(
109+
i
110+
for i in range(1, fm_end)
111+
if re.match(rf"^{re.escape(key)}:( |$)", lines[i].rstrip("\n"))
112+
),
113+
None,
114+
)
106115
if key_idx is None:
107116
return False
108117

109-
rest = lines[key_idx].rstrip("\n")[len(key) + 1:].strip()
118+
rest = lines[key_idx].rstrip("\n")[len(key) + 1 :].strip()
110119
if rest:
111120
# scalar form: upgrade to a list, keeping the existing value first
112121
if rest == quoted_item:
@@ -136,7 +145,9 @@ def set_scalar_if_empty(path, key, value):
136145
lines = f.read().splitlines(keepends=True)
137146
if not lines or lines[0].rstrip("\n") != "---":
138147
return False
139-
fm_end = next((i for i in range(1, len(lines)) if lines[i].rstrip("\n") == "---"), None)
148+
fm_end = next(
149+
(i for i in range(1, len(lines)) if lines[i].rstrip("\n") == "---"), None
150+
)
140151
if fm_end is None:
141152
return False
142153
for i in range(1, fm_end):

treefmt.nix

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
# Whole-tree formatter. `nix fmt` formats; `nix flake check` verifies (see the
3+
# `formatting` check in flake.nix). Add a language by enabling its program here.
4+
projectRootFile = "flake.nix";
5+
6+
programs = {
7+
nixfmt.enable = true; # nix
8+
ruff-format.enable = true; # python (packages/vault-sync)
9+
shfmt.enable = true; # shell (modules/exedev/activate.sh)
10+
prettier.enable = true; # yaml (.github/workflows), markdown, json
11+
};
12+
13+
# Keep `case` bodies indented (shfmt's `-ci`); its default de-indents them.
14+
# List-typed, so this concatenates with the shfmt module's own flags.
15+
settings.formatter.shfmt.options = [ "-ci" ];
16+
17+
settings.global.excludes = [
18+
# Generated / externally-managed lock files — never hand-format.
19+
"flake.lock"
20+
"packages/obsidian-headless/package-lock.json"
21+
"packages/pi/npm-shrinkwrap.json"
22+
];
23+
}

0 commit comments

Comments
 (0)