├── .editorconfig
├── .github
├── renovate.json
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── files
├── figma-agent.service
├── figma-agent.socket
└── install.sh
├── rust-toolchain.toml
├── rustfmt.toml
└── src
├── config.rs
├── font.rs
├── lib.rs
├── main.rs
├── path.rs
├── payload.rs
├── renderer.rs
├── routes.rs
└── scanner.rs
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = space
7 | indent_size = 2
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
14 | [*.rs]
15 | indent_size = 4
16 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["github>neetly/renovate-config"]
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | permissions:
6 | contents: read
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v5
14 | - uses: moonrepo/setup-rust@v1
15 | with:
16 | inherit-toolchain: true
17 | - run: rustup component add rustfmt --toolchain nightly
18 |
19 | - run: cargo fetch --locked
20 | - run: cargo +nightly fmt --check
21 | - run: cargo clippy -- --deny warnings
22 | - run: cargo test
23 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags: ["**"]
6 |
7 | permissions:
8 | contents: write
9 |
10 | jobs:
11 | build:
12 | strategy:
13 | matrix:
14 | include:
15 | - target: x86_64-unknown-linux-gnu
16 | runner: ubuntu-22.04
17 | - target: aarch64-unknown-linux-gnu
18 | runner: ubuntu-22.04-arm
19 |
20 | runs-on: ${{ matrix.runner }}
21 |
22 | steps:
23 | - uses: actions/checkout@v5
24 | - uses: moonrepo/setup-rust@v1
25 | with:
26 | inherit-toolchain: true
27 |
28 | - run: cargo fetch --locked --target "${{ matrix.target }}"
29 | - run: cargo build --release --target "${{ matrix.target }}"
30 |
31 | - run: |
32 | mkdir ./release
33 | cp "./target/${{ matrix.target }}/release/figma-agent" \
34 | "./release/figma-agent-${{ matrix.target }}"
35 |
36 | - uses: actions/upload-artifact@v5
37 | with:
38 | name: ${{ matrix.target }}
39 | path: ./release/figma-agent-${{ matrix.target }}
40 |
41 | release:
42 | needs: build
43 |
44 | runs-on: ubuntu-latest
45 |
46 | steps:
47 | - uses: actions/download-artifact@v6
48 | with:
49 | path: ./release
50 | merge-multiple: true
51 | - uses: softprops/action-gh-release@v2
52 | with:
53 | name: Figma Agent for Linux ${{ github.ref_name }}
54 | prerelease: ${{ contains(github.ref_name, '-') }}
55 | files: ./release/figma-agent-*
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Artifacts
2 | /target/
3 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "adler2"
7 | version = "2.0.1"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
10 |
11 | [[package]]
12 | name = "alloc-no-stdlib"
13 | version = "2.0.4"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
16 |
17 | [[package]]
18 | name = "alloc-stdlib"
19 | version = "0.2.2"
20 | source = "registry+https://github.com/rust-lang/crates.io-index"
21 | checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
22 | dependencies = [
23 | "alloc-no-stdlib",
24 | ]
25 |
26 | [[package]]
27 | name = "anyhow"
28 | version = "1.0.100"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
31 |
32 | [[package]]
33 | name = "async-compression"
34 | version = "0.4.32"
35 | source = "registry+https://github.com/rust-lang/crates.io-index"
36 | checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0"
37 | dependencies = [
38 | "compression-codecs",
39 | "compression-core",
40 | "futures-core",
41 | "pin-project-lite",
42 | "tokio",
43 | ]
44 |
45 | [[package]]
46 | name = "atomic-waker"
47 | version = "1.1.2"
48 | source = "registry+https://github.com/rust-lang/crates.io-index"
49 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
50 |
51 | [[package]]
52 | name = "autocfg"
53 | version = "1.5.0"
54 | source = "registry+https://github.com/rust-lang/crates.io-index"
55 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
56 |
57 | [[package]]
58 | name = "axum"
59 | version = "0.8.6"
60 | source = "registry+https://github.com/rust-lang/crates.io-index"
61 | checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
62 | dependencies = [
63 | "axum-core",
64 | "bytes",
65 | "form_urlencoded",
66 | "futures-util",
67 | "http",
68 | "http-body",
69 | "http-body-util",
70 | "hyper",
71 | "hyper-util",
72 | "itoa",
73 | "matchit",
74 | "memchr",
75 | "mime",
76 | "percent-encoding",
77 | "pin-project-lite",
78 | "serde_core",
79 | "serde_json",
80 | "serde_path_to_error",
81 | "serde_urlencoded",
82 | "sync_wrapper",
83 | "tokio",
84 | "tower",
85 | "tower-layer",
86 | "tower-service",
87 | "tracing",
88 | ]
89 |
90 | [[package]]
91 | name = "axum-core"
92 | version = "0.5.5"
93 | source = "registry+https://github.com/rust-lang/crates.io-index"
94 | checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
95 | dependencies = [
96 | "bytes",
97 | "futures-core",
98 | "http",
99 | "http-body",
100 | "http-body-util",
101 | "mime",
102 | "pin-project-lite",
103 | "sync_wrapper",
104 | "tower-layer",
105 | "tower-service",
106 | "tracing",
107 | ]
108 |
109 | [[package]]
110 | name = "base64"
111 | version = "0.22.1"
112 | source = "registry+https://github.com/rust-lang/crates.io-index"
113 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
114 |
115 | [[package]]
116 | name = "bitflags"
117 | version = "2.10.0"
118 | source = "registry+https://github.com/rust-lang/crates.io-index"
119 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
120 |
121 | [[package]]
122 | name = "brotli"
123 | version = "8.0.2"
124 | source = "registry+https://github.com/rust-lang/crates.io-index"
125 | checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
126 | dependencies = [
127 | "alloc-no-stdlib",
128 | "alloc-stdlib",
129 | "brotli-decompressor",
130 | ]
131 |
132 | [[package]]
133 | name = "brotli-decompressor"
134 | version = "5.0.0"
135 | source = "registry+https://github.com/rust-lang/crates.io-index"
136 | checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
137 | dependencies = [
138 | "alloc-no-stdlib",
139 | "alloc-stdlib",
140 | ]
141 |
142 | [[package]]
143 | name = "bumpalo"
144 | version = "3.19.0"
145 | source = "registry+https://github.com/rust-lang/crates.io-index"
146 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
147 |
148 | [[package]]
149 | name = "bytemuck"
150 | version = "1.24.0"
151 | source = "registry+https://github.com/rust-lang/crates.io-index"
152 | checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
153 | dependencies = [
154 | "bytemuck_derive",
155 | ]
156 |
157 | [[package]]
158 | name = "bytemuck_derive"
159 | version = "1.10.2"
160 | source = "registry+https://github.com/rust-lang/crates.io-index"
161 | checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
162 | dependencies = [
163 | "proc-macro2",
164 | "quote",
165 | "syn",
166 | ]
167 |
168 | [[package]]
169 | name = "byteorder"
170 | version = "1.5.0"
171 | source = "registry+https://github.com/rust-lang/crates.io-index"
172 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
173 |
174 | [[package]]
175 | name = "bytes"
176 | version = "1.10.1"
177 | source = "registry+https://github.com/rust-lang/crates.io-index"
178 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
179 |
180 | [[package]]
181 | name = "cc"
182 | version = "1.2.44"
183 | source = "registry+https://github.com/rust-lang/crates.io-index"
184 | checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3"
185 | dependencies = [
186 | "find-msvc-tools",
187 | "jobserver",
188 | "libc",
189 | "shlex",
190 | ]
191 |
192 | [[package]]
193 | name = "cfg-if"
194 | version = "1.0.4"
195 | source = "registry+https://github.com/rust-lang/crates.io-index"
196 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
197 |
198 | [[package]]
199 | name = "compression-codecs"
200 | version = "0.4.31"
201 | source = "registry+https://github.com/rust-lang/crates.io-index"
202 | checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23"
203 | dependencies = [
204 | "brotli",
205 | "compression-core",
206 | "flate2",
207 | "memchr",
208 | "zstd",
209 | "zstd-safe",
210 | ]
211 |
212 | [[package]]
213 | name = "compression-core"
214 | version = "0.4.29"
215 | source = "registry+https://github.com/rust-lang/crates.io-index"
216 | checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb"
217 |
218 | [[package]]
219 | name = "core_maths"
220 | version = "0.1.1"
221 | source = "registry+https://github.com/rust-lang/crates.io-index"
222 | checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
223 | dependencies = [
224 | "libm",
225 | ]
226 |
227 | [[package]]
228 | name = "crc32fast"
229 | version = "1.5.0"
230 | source = "registry+https://github.com/rust-lang/crates.io-index"
231 | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
232 | dependencies = [
233 | "cfg-if",
234 | ]
235 |
236 | [[package]]
237 | name = "either"
238 | version = "1.15.0"
239 | source = "registry+https://github.com/rust-lang/crates.io-index"
240 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
241 |
242 | [[package]]
243 | name = "equivalent"
244 | version = "1.0.2"
245 | source = "registry+https://github.com/rust-lang/crates.io-index"
246 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
247 |
248 | [[package]]
249 | name = "figma-agent"
250 | version = "0.4.2"
251 | dependencies = [
252 | "anyhow",
253 | "axum",
254 | "fontconfig-parser",
255 | "harfrust",
256 | "interp",
257 | "itertools",
258 | "jsonc-parser",
259 | "listenfd",
260 | "read-fonts",
261 | "serde",
262 | "serde_json",
263 | "skrifa",
264 | "svg",
265 | "thiserror",
266 | "tokio",
267 | "tower",
268 | "tower-http",
269 | "tracing",
270 | "tracing-subscriber",
271 | "walkdir",
272 | "xdg",
273 | ]
274 |
275 | [[package]]
276 | name = "find-msvc-tools"
277 | version = "0.1.4"
278 | source = "registry+https://github.com/rust-lang/crates.io-index"
279 | checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
280 |
281 | [[package]]
282 | name = "flate2"
283 | version = "1.1.5"
284 | source = "registry+https://github.com/rust-lang/crates.io-index"
285 | checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
286 | dependencies = [
287 | "crc32fast",
288 | "miniz_oxide",
289 | ]
290 |
291 | [[package]]
292 | name = "fnv"
293 | version = "1.0.7"
294 | source = "registry+https://github.com/rust-lang/crates.io-index"
295 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
296 |
297 | [[package]]
298 | name = "font-types"
299 | version = "0.10.0"
300 | source = "registry+https://github.com/rust-lang/crates.io-index"
301 | checksum = "511e2c18a516c666d27867d2f9821f76e7d591f762e9fc41dd6cc5c90fe54b0b"
302 | dependencies = [
303 | "bytemuck",
304 | ]
305 |
306 | [[package]]
307 | name = "fontconfig-parser"
308 | version = "0.5.8"
309 | source = "registry+https://github.com/rust-lang/crates.io-index"
310 | checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
311 | dependencies = [
312 | "roxmltree",
313 | ]
314 |
315 | [[package]]
316 | name = "form_urlencoded"
317 | version = "1.2.2"
318 | source = "registry+https://github.com/rust-lang/crates.io-index"
319 | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
320 | dependencies = [
321 | "percent-encoding",
322 | ]
323 |
324 | [[package]]
325 | name = "futures-channel"
326 | version = "0.3.31"
327 | source = "registry+https://github.com/rust-lang/crates.io-index"
328 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
329 | dependencies = [
330 | "futures-core",
331 | ]
332 |
333 | [[package]]
334 | name = "futures-core"
335 | version = "0.3.31"
336 | source = "registry+https://github.com/rust-lang/crates.io-index"
337 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
338 |
339 | [[package]]
340 | name = "futures-sink"
341 | version = "0.3.31"
342 | source = "registry+https://github.com/rust-lang/crates.io-index"
343 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
344 |
345 | [[package]]
346 | name = "futures-task"
347 | version = "0.3.31"
348 | source = "registry+https://github.com/rust-lang/crates.io-index"
349 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
350 |
351 | [[package]]
352 | name = "futures-util"
353 | version = "0.3.31"
354 | source = "registry+https://github.com/rust-lang/crates.io-index"
355 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
356 | dependencies = [
357 | "futures-core",
358 | "futures-task",
359 | "pin-project-lite",
360 | "pin-utils",
361 | "slab",
362 | ]
363 |
364 | [[package]]
365 | name = "getrandom"
366 | version = "0.3.4"
367 | source = "registry+https://github.com/rust-lang/crates.io-index"
368 | checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
369 | dependencies = [
370 | "cfg-if",
371 | "libc",
372 | "r-efi",
373 | "wasip2",
374 | ]
375 |
376 | [[package]]
377 | name = "harfrust"
378 | version = "0.3.2"
379 | source = "registry+https://github.com/rust-lang/crates.io-index"
380 | checksum = "92c020db12c71d8a12a3fe7607873cade3a01a6287e29d540c8723276221b9d8"
381 | dependencies = [
382 | "bitflags",
383 | "bytemuck",
384 | "core_maths",
385 | "read-fonts",
386 | "smallvec",
387 | ]
388 |
389 | [[package]]
390 | name = "hashbrown"
391 | version = "0.16.0"
392 | source = "registry+https://github.com/rust-lang/crates.io-index"
393 | checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
394 |
395 | [[package]]
396 | name = "hdrhistogram"
397 | version = "7.5.4"
398 | source = "registry+https://github.com/rust-lang/crates.io-index"
399 | checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d"
400 | dependencies = [
401 | "byteorder",
402 | "num-traits",
403 | ]
404 |
405 | [[package]]
406 | name = "http"
407 | version = "1.3.1"
408 | source = "registry+https://github.com/rust-lang/crates.io-index"
409 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
410 | dependencies = [
411 | "bytes",
412 | "fnv",
413 | "itoa",
414 | ]
415 |
416 | [[package]]
417 | name = "http-body"
418 | version = "1.0.1"
419 | source = "registry+https://github.com/rust-lang/crates.io-index"
420 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
421 | dependencies = [
422 | "bytes",
423 | "http",
424 | ]
425 |
426 | [[package]]
427 | name = "http-body-util"
428 | version = "0.1.3"
429 | source = "registry+https://github.com/rust-lang/crates.io-index"
430 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
431 | dependencies = [
432 | "bytes",
433 | "futures-core",
434 | "http",
435 | "http-body",
436 | "pin-project-lite",
437 | ]
438 |
439 | [[package]]
440 | name = "http-range-header"
441 | version = "0.4.2"
442 | source = "registry+https://github.com/rust-lang/crates.io-index"
443 | checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
444 |
445 | [[package]]
446 | name = "httparse"
447 | version = "1.10.1"
448 | source = "registry+https://github.com/rust-lang/crates.io-index"
449 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
450 |
451 | [[package]]
452 | name = "httpdate"
453 | version = "1.0.3"
454 | source = "registry+https://github.com/rust-lang/crates.io-index"
455 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
456 |
457 | [[package]]
458 | name = "hyper"
459 | version = "1.7.0"
460 | source = "registry+https://github.com/rust-lang/crates.io-index"
461 | checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
462 | dependencies = [
463 | "atomic-waker",
464 | "bytes",
465 | "futures-channel",
466 | "futures-core",
467 | "http",
468 | "http-body",
469 | "httparse",
470 | "httpdate",
471 | "itoa",
472 | "pin-project-lite",
473 | "pin-utils",
474 | "smallvec",
475 | "tokio",
476 | ]
477 |
478 | [[package]]
479 | name = "hyper-util"
480 | version = "0.1.17"
481 | source = "registry+https://github.com/rust-lang/crates.io-index"
482 | checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
483 | dependencies = [
484 | "bytes",
485 | "futures-core",
486 | "http",
487 | "http-body",
488 | "hyper",
489 | "pin-project-lite",
490 | "tokio",
491 | "tower-service",
492 | ]
493 |
494 | [[package]]
495 | name = "indexmap"
496 | version = "2.12.0"
497 | source = "registry+https://github.com/rust-lang/crates.io-index"
498 | checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
499 | dependencies = [
500 | "equivalent",
501 | "hashbrown",
502 | ]
503 |
504 | [[package]]
505 | name = "interp"
506 | version = "2.1.1"
507 | source = "registry+https://github.com/rust-lang/crates.io-index"
508 | checksum = "433698c934a80497f6a2c37d6ce8398e70e0a5b8a50335e75d45f79d22259c26"
509 | dependencies = [
510 | "itertools",
511 | "num-traits",
512 | ]
513 |
514 | [[package]]
515 | name = "iri-string"
516 | version = "0.7.8"
517 | source = "registry+https://github.com/rust-lang/crates.io-index"
518 | checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
519 | dependencies = [
520 | "memchr",
521 | "serde",
522 | ]
523 |
524 | [[package]]
525 | name = "itertools"
526 | version = "0.14.0"
527 | source = "registry+https://github.com/rust-lang/crates.io-index"
528 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
529 | dependencies = [
530 | "either",
531 | ]
532 |
533 | [[package]]
534 | name = "itoa"
535 | version = "1.0.15"
536 | source = "registry+https://github.com/rust-lang/crates.io-index"
537 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
538 |
539 | [[package]]
540 | name = "jobserver"
541 | version = "0.1.34"
542 | source = "registry+https://github.com/rust-lang/crates.io-index"
543 | checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
544 | dependencies = [
545 | "getrandom",
546 | "libc",
547 | ]
548 |
549 | [[package]]
550 | name = "js-sys"
551 | version = "0.3.82"
552 | source = "registry+https://github.com/rust-lang/crates.io-index"
553 | checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
554 | dependencies = [
555 | "once_cell",
556 | "wasm-bindgen",
557 | ]
558 |
559 | [[package]]
560 | name = "jsonc-parser"
561 | version = "0.27.0"
562 | source = "registry+https://github.com/rust-lang/crates.io-index"
563 | checksum = "7ec4ac49f13c7b00f435f8a5bb55d725705e2cf620df35a5859321595102eb7e"
564 | dependencies = [
565 | "serde_json",
566 | ]
567 |
568 | [[package]]
569 | name = "lazy_static"
570 | version = "1.5.0"
571 | source = "registry+https://github.com/rust-lang/crates.io-index"
572 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
573 |
574 | [[package]]
575 | name = "libc"
576 | version = "0.2.177"
577 | source = "registry+https://github.com/rust-lang/crates.io-index"
578 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
579 |
580 | [[package]]
581 | name = "libm"
582 | version = "0.2.15"
583 | source = "registry+https://github.com/rust-lang/crates.io-index"
584 | checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
585 |
586 | [[package]]
587 | name = "listenfd"
588 | version = "1.0.2"
589 | source = "registry+https://github.com/rust-lang/crates.io-index"
590 | checksum = "b87bc54a4629b4294d0b3ef041b64c40c611097a677d9dc07b2c67739fe39dba"
591 | dependencies = [
592 | "libc",
593 | "uuid",
594 | "winapi",
595 | ]
596 |
597 | [[package]]
598 | name = "lock_api"
599 | version = "0.4.14"
600 | source = "registry+https://github.com/rust-lang/crates.io-index"
601 | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
602 | dependencies = [
603 | "scopeguard",
604 | ]
605 |
606 | [[package]]
607 | name = "log"
608 | version = "0.4.28"
609 | source = "registry+https://github.com/rust-lang/crates.io-index"
610 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
611 |
612 | [[package]]
613 | name = "matchit"
614 | version = "0.8.4"
615 | source = "registry+https://github.com/rust-lang/crates.io-index"
616 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
617 |
618 | [[package]]
619 | name = "memchr"
620 | version = "2.7.6"
621 | source = "registry+https://github.com/rust-lang/crates.io-index"
622 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
623 |
624 | [[package]]
625 | name = "mime"
626 | version = "0.3.17"
627 | source = "registry+https://github.com/rust-lang/crates.io-index"
628 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
629 |
630 | [[package]]
631 | name = "mime_guess"
632 | version = "2.0.5"
633 | source = "registry+https://github.com/rust-lang/crates.io-index"
634 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
635 | dependencies = [
636 | "mime",
637 | "unicase",
638 | ]
639 |
640 | [[package]]
641 | name = "miniz_oxide"
642 | version = "0.8.9"
643 | source = "registry+https://github.com/rust-lang/crates.io-index"
644 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
645 | dependencies = [
646 | "adler2",
647 | "simd-adler32",
648 | ]
649 |
650 | [[package]]
651 | name = "mio"
652 | version = "1.1.0"
653 | source = "registry+https://github.com/rust-lang/crates.io-index"
654 | checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
655 | dependencies = [
656 | "libc",
657 | "wasi",
658 | "windows-sys 0.61.2",
659 | ]
660 |
661 | [[package]]
662 | name = "nu-ansi-term"
663 | version = "0.50.3"
664 | source = "registry+https://github.com/rust-lang/crates.io-index"
665 | checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
666 | dependencies = [
667 | "windows-sys 0.61.2",
668 | ]
669 |
670 | [[package]]
671 | name = "num-traits"
672 | version = "0.2.19"
673 | source = "registry+https://github.com/rust-lang/crates.io-index"
674 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
675 | dependencies = [
676 | "autocfg",
677 | ]
678 |
679 | [[package]]
680 | name = "once_cell"
681 | version = "1.21.3"
682 | source = "registry+https://github.com/rust-lang/crates.io-index"
683 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
684 |
685 | [[package]]
686 | name = "parking_lot"
687 | version = "0.12.5"
688 | source = "registry+https://github.com/rust-lang/crates.io-index"
689 | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
690 | dependencies = [
691 | "lock_api",
692 | "parking_lot_core",
693 | ]
694 |
695 | [[package]]
696 | name = "parking_lot_core"
697 | version = "0.9.12"
698 | source = "registry+https://github.com/rust-lang/crates.io-index"
699 | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
700 | dependencies = [
701 | "cfg-if",
702 | "libc",
703 | "redox_syscall",
704 | "smallvec",
705 | "windows-link",
706 | ]
707 |
708 | [[package]]
709 | name = "percent-encoding"
710 | version = "2.3.2"
711 | source = "registry+https://github.com/rust-lang/crates.io-index"
712 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
713 |
714 | [[package]]
715 | name = "pin-project-lite"
716 | version = "0.2.16"
717 | source = "registry+https://github.com/rust-lang/crates.io-index"
718 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
719 |
720 | [[package]]
721 | name = "pin-utils"
722 | version = "0.1.0"
723 | source = "registry+https://github.com/rust-lang/crates.io-index"
724 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
725 |
726 | [[package]]
727 | name = "pkg-config"
728 | version = "0.3.32"
729 | source = "registry+https://github.com/rust-lang/crates.io-index"
730 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
731 |
732 | [[package]]
733 | name = "proc-macro2"
734 | version = "1.0.103"
735 | source = "registry+https://github.com/rust-lang/crates.io-index"
736 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
737 | dependencies = [
738 | "unicode-ident",
739 | ]
740 |
741 | [[package]]
742 | name = "quote"
743 | version = "1.0.41"
744 | source = "registry+https://github.com/rust-lang/crates.io-index"
745 | checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
746 | dependencies = [
747 | "proc-macro2",
748 | ]
749 |
750 | [[package]]
751 | name = "r-efi"
752 | version = "5.3.0"
753 | source = "registry+https://github.com/rust-lang/crates.io-index"
754 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
755 |
756 | [[package]]
757 | name = "read-fonts"
758 | version = "0.35.0"
759 | source = "registry+https://github.com/rust-lang/crates.io-index"
760 | checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358"
761 | dependencies = [
762 | "bytemuck",
763 | "core_maths",
764 | "font-types",
765 | ]
766 |
767 | [[package]]
768 | name = "redox_syscall"
769 | version = "0.5.18"
770 | source = "registry+https://github.com/rust-lang/crates.io-index"
771 | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
772 | dependencies = [
773 | "bitflags",
774 | ]
775 |
776 | [[package]]
777 | name = "roxmltree"
778 | version = "0.20.0"
779 | source = "registry+https://github.com/rust-lang/crates.io-index"
780 | checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
781 |
782 | [[package]]
783 | name = "rustversion"
784 | version = "1.0.22"
785 | source = "registry+https://github.com/rust-lang/crates.io-index"
786 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
787 |
788 | [[package]]
789 | name = "ryu"
790 | version = "1.0.20"
791 | source = "registry+https://github.com/rust-lang/crates.io-index"
792 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
793 |
794 | [[package]]
795 | name = "same-file"
796 | version = "1.0.6"
797 | source = "registry+https://github.com/rust-lang/crates.io-index"
798 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
799 | dependencies = [
800 | "winapi-util",
801 | ]
802 |
803 | [[package]]
804 | name = "scopeguard"
805 | version = "1.2.0"
806 | source = "registry+https://github.com/rust-lang/crates.io-index"
807 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
808 |
809 | [[package]]
810 | name = "serde"
811 | version = "1.0.228"
812 | source = "registry+https://github.com/rust-lang/crates.io-index"
813 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
814 | dependencies = [
815 | "serde_core",
816 | "serde_derive",
817 | ]
818 |
819 | [[package]]
820 | name = "serde_core"
821 | version = "1.0.228"
822 | source = "registry+https://github.com/rust-lang/crates.io-index"
823 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
824 | dependencies = [
825 | "serde_derive",
826 | ]
827 |
828 | [[package]]
829 | name = "serde_derive"
830 | version = "1.0.228"
831 | source = "registry+https://github.com/rust-lang/crates.io-index"
832 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
833 | dependencies = [
834 | "proc-macro2",
835 | "quote",
836 | "syn",
837 | ]
838 |
839 | [[package]]
840 | name = "serde_json"
841 | version = "1.0.145"
842 | source = "registry+https://github.com/rust-lang/crates.io-index"
843 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
844 | dependencies = [
845 | "itoa",
846 | "memchr",
847 | "ryu",
848 | "serde",
849 | "serde_core",
850 | ]
851 |
852 | [[package]]
853 | name = "serde_path_to_error"
854 | version = "0.1.20"
855 | source = "registry+https://github.com/rust-lang/crates.io-index"
856 | checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
857 | dependencies = [
858 | "itoa",
859 | "serde",
860 | "serde_core",
861 | ]
862 |
863 | [[package]]
864 | name = "serde_urlencoded"
865 | version = "0.7.1"
866 | source = "registry+https://github.com/rust-lang/crates.io-index"
867 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
868 | dependencies = [
869 | "form_urlencoded",
870 | "itoa",
871 | "ryu",
872 | "serde",
873 | ]
874 |
875 | [[package]]
876 | name = "sharded-slab"
877 | version = "0.1.7"
878 | source = "registry+https://github.com/rust-lang/crates.io-index"
879 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
880 | dependencies = [
881 | "lazy_static",
882 | ]
883 |
884 | [[package]]
885 | name = "shlex"
886 | version = "1.3.0"
887 | source = "registry+https://github.com/rust-lang/crates.io-index"
888 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
889 |
890 | [[package]]
891 | name = "signal-hook-registry"
892 | version = "1.4.6"
893 | source = "registry+https://github.com/rust-lang/crates.io-index"
894 | checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
895 | dependencies = [
896 | "libc",
897 | ]
898 |
899 | [[package]]
900 | name = "simd-adler32"
901 | version = "0.3.7"
902 | source = "registry+https://github.com/rust-lang/crates.io-index"
903 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
904 |
905 | [[package]]
906 | name = "skrifa"
907 | version = "0.37.0"
908 | source = "registry+https://github.com/rust-lang/crates.io-index"
909 | checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841"
910 | dependencies = [
911 | "bytemuck",
912 | "read-fonts",
913 | ]
914 |
915 | [[package]]
916 | name = "slab"
917 | version = "0.4.11"
918 | source = "registry+https://github.com/rust-lang/crates.io-index"
919 | checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
920 |
921 | [[package]]
922 | name = "smallvec"
923 | version = "1.15.1"
924 | source = "registry+https://github.com/rust-lang/crates.io-index"
925 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
926 |
927 | [[package]]
928 | name = "socket2"
929 | version = "0.6.1"
930 | source = "registry+https://github.com/rust-lang/crates.io-index"
931 | checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
932 | dependencies = [
933 | "libc",
934 | "windows-sys 0.60.2",
935 | ]
936 |
937 | [[package]]
938 | name = "svg"
939 | version = "0.18.0"
940 | source = "registry+https://github.com/rust-lang/crates.io-index"
941 | checksum = "94afda9cd163c04f6bee8b4bf2501c91548deae308373c436f36aeff3cf3c4a3"
942 |
943 | [[package]]
944 | name = "syn"
945 | version = "2.0.108"
946 | source = "registry+https://github.com/rust-lang/crates.io-index"
947 | checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
948 | dependencies = [
949 | "proc-macro2",
950 | "quote",
951 | "unicode-ident",
952 | ]
953 |
954 | [[package]]
955 | name = "sync_wrapper"
956 | version = "1.0.2"
957 | source = "registry+https://github.com/rust-lang/crates.io-index"
958 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
959 |
960 | [[package]]
961 | name = "thiserror"
962 | version = "2.0.17"
963 | source = "registry+https://github.com/rust-lang/crates.io-index"
964 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
965 | dependencies = [
966 | "thiserror-impl",
967 | ]
968 |
969 | [[package]]
970 | name = "thiserror-impl"
971 | version = "2.0.17"
972 | source = "registry+https://github.com/rust-lang/crates.io-index"
973 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
974 | dependencies = [
975 | "proc-macro2",
976 | "quote",
977 | "syn",
978 | ]
979 |
980 | [[package]]
981 | name = "thread_local"
982 | version = "1.1.9"
983 | source = "registry+https://github.com/rust-lang/crates.io-index"
984 | checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
985 | dependencies = [
986 | "cfg-if",
987 | ]
988 |
989 | [[package]]
990 | name = "tokio"
991 | version = "1.48.0"
992 | source = "registry+https://github.com/rust-lang/crates.io-index"
993 | checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
994 | dependencies = [
995 | "bytes",
996 | "libc",
997 | "mio",
998 | "parking_lot",
999 | "pin-project-lite",
1000 | "signal-hook-registry",
1001 | "socket2",
1002 | "tokio-macros",
1003 | "windows-sys 0.61.2",
1004 | ]
1005 |
1006 | [[package]]
1007 | name = "tokio-macros"
1008 | version = "2.6.0"
1009 | source = "registry+https://github.com/rust-lang/crates.io-index"
1010 | checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
1011 | dependencies = [
1012 | "proc-macro2",
1013 | "quote",
1014 | "syn",
1015 | ]
1016 |
1017 | [[package]]
1018 | name = "tokio-util"
1019 | version = "0.7.16"
1020 | source = "registry+https://github.com/rust-lang/crates.io-index"
1021 | checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
1022 | dependencies = [
1023 | "bytes",
1024 | "futures-core",
1025 | "futures-sink",
1026 | "pin-project-lite",
1027 | "tokio",
1028 | ]
1029 |
1030 | [[package]]
1031 | name = "tower"
1032 | version = "0.5.2"
1033 | source = "registry+https://github.com/rust-lang/crates.io-index"
1034 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
1035 | dependencies = [
1036 | "futures-core",
1037 | "futures-util",
1038 | "hdrhistogram",
1039 | "indexmap",
1040 | "pin-project-lite",
1041 | "slab",
1042 | "sync_wrapper",
1043 | "tokio",
1044 | "tokio-util",
1045 | "tower-layer",
1046 | "tower-service",
1047 | "tracing",
1048 | ]
1049 |
1050 | [[package]]
1051 | name = "tower-http"
1052 | version = "0.6.6"
1053 | source = "registry+https://github.com/rust-lang/crates.io-index"
1054 | checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
1055 | dependencies = [
1056 | "async-compression",
1057 | "base64",
1058 | "bitflags",
1059 | "bytes",
1060 | "futures-core",
1061 | "futures-util",
1062 | "http",
1063 | "http-body",
1064 | "http-body-util",
1065 | "http-range-header",
1066 | "httpdate",
1067 | "iri-string",
1068 | "mime",
1069 | "mime_guess",
1070 | "percent-encoding",
1071 | "pin-project-lite",
1072 | "tokio",
1073 | "tokio-util",
1074 | "tower",
1075 | "tower-layer",
1076 | "tower-service",
1077 | "tracing",
1078 | "uuid",
1079 | ]
1080 |
1081 | [[package]]
1082 | name = "tower-layer"
1083 | version = "0.3.3"
1084 | source = "registry+https://github.com/rust-lang/crates.io-index"
1085 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
1086 |
1087 | [[package]]
1088 | name = "tower-service"
1089 | version = "0.3.3"
1090 | source = "registry+https://github.com/rust-lang/crates.io-index"
1091 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
1092 |
1093 | [[package]]
1094 | name = "tracing"
1095 | version = "0.1.41"
1096 | source = "registry+https://github.com/rust-lang/crates.io-index"
1097 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
1098 | dependencies = [
1099 | "log",
1100 | "pin-project-lite",
1101 | "tracing-attributes",
1102 | "tracing-core",
1103 | ]
1104 |
1105 | [[package]]
1106 | name = "tracing-attributes"
1107 | version = "0.1.30"
1108 | source = "registry+https://github.com/rust-lang/crates.io-index"
1109 | checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
1110 | dependencies = [
1111 | "proc-macro2",
1112 | "quote",
1113 | "syn",
1114 | ]
1115 |
1116 | [[package]]
1117 | name = "tracing-core"
1118 | version = "0.1.34"
1119 | source = "registry+https://github.com/rust-lang/crates.io-index"
1120 | checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
1121 | dependencies = [
1122 | "once_cell",
1123 | "valuable",
1124 | ]
1125 |
1126 | [[package]]
1127 | name = "tracing-log"
1128 | version = "0.2.0"
1129 | source = "registry+https://github.com/rust-lang/crates.io-index"
1130 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
1131 | dependencies = [
1132 | "log",
1133 | "once_cell",
1134 | "tracing-core",
1135 | ]
1136 |
1137 | [[package]]
1138 | name = "tracing-subscriber"
1139 | version = "0.3.20"
1140 | source = "registry+https://github.com/rust-lang/crates.io-index"
1141 | checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
1142 | dependencies = [
1143 | "nu-ansi-term",
1144 | "sharded-slab",
1145 | "smallvec",
1146 | "thread_local",
1147 | "tracing-core",
1148 | "tracing-log",
1149 | ]
1150 |
1151 | [[package]]
1152 | name = "unicase"
1153 | version = "2.8.1"
1154 | source = "registry+https://github.com/rust-lang/crates.io-index"
1155 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
1156 |
1157 | [[package]]
1158 | name = "unicode-ident"
1159 | version = "1.0.22"
1160 | source = "registry+https://github.com/rust-lang/crates.io-index"
1161 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
1162 |
1163 | [[package]]
1164 | name = "uuid"
1165 | version = "1.18.1"
1166 | source = "registry+https://github.com/rust-lang/crates.io-index"
1167 | checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
1168 | dependencies = [
1169 | "getrandom",
1170 | "js-sys",
1171 | "wasm-bindgen",
1172 | ]
1173 |
1174 | [[package]]
1175 | name = "valuable"
1176 | version = "0.1.1"
1177 | source = "registry+https://github.com/rust-lang/crates.io-index"
1178 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
1179 |
1180 | [[package]]
1181 | name = "walkdir"
1182 | version = "2.5.0"
1183 | source = "registry+https://github.com/rust-lang/crates.io-index"
1184 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
1185 | dependencies = [
1186 | "same-file",
1187 | "winapi-util",
1188 | ]
1189 |
1190 | [[package]]
1191 | name = "wasi"
1192 | version = "0.11.1+wasi-snapshot-preview1"
1193 | source = "registry+https://github.com/rust-lang/crates.io-index"
1194 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
1195 |
1196 | [[package]]
1197 | name = "wasip2"
1198 | version = "1.0.1+wasi-0.2.4"
1199 | source = "registry+https://github.com/rust-lang/crates.io-index"
1200 | checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
1201 | dependencies = [
1202 | "wit-bindgen",
1203 | ]
1204 |
1205 | [[package]]
1206 | name = "wasm-bindgen"
1207 | version = "0.2.105"
1208 | source = "registry+https://github.com/rust-lang/crates.io-index"
1209 | checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
1210 | dependencies = [
1211 | "cfg-if",
1212 | "once_cell",
1213 | "rustversion",
1214 | "wasm-bindgen-macro",
1215 | "wasm-bindgen-shared",
1216 | ]
1217 |
1218 | [[package]]
1219 | name = "wasm-bindgen-macro"
1220 | version = "0.2.105"
1221 | source = "registry+https://github.com/rust-lang/crates.io-index"
1222 | checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
1223 | dependencies = [
1224 | "quote",
1225 | "wasm-bindgen-macro-support",
1226 | ]
1227 |
1228 | [[package]]
1229 | name = "wasm-bindgen-macro-support"
1230 | version = "0.2.105"
1231 | source = "registry+https://github.com/rust-lang/crates.io-index"
1232 | checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
1233 | dependencies = [
1234 | "bumpalo",
1235 | "proc-macro2",
1236 | "quote",
1237 | "syn",
1238 | "wasm-bindgen-shared",
1239 | ]
1240 |
1241 | [[package]]
1242 | name = "wasm-bindgen-shared"
1243 | version = "0.2.105"
1244 | source = "registry+https://github.com/rust-lang/crates.io-index"
1245 | checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
1246 | dependencies = [
1247 | "unicode-ident",
1248 | ]
1249 |
1250 | [[package]]
1251 | name = "winapi"
1252 | version = "0.3.9"
1253 | source = "registry+https://github.com/rust-lang/crates.io-index"
1254 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1255 | dependencies = [
1256 | "winapi-i686-pc-windows-gnu",
1257 | "winapi-x86_64-pc-windows-gnu",
1258 | ]
1259 |
1260 | [[package]]
1261 | name = "winapi-i686-pc-windows-gnu"
1262 | version = "0.4.0"
1263 | source = "registry+https://github.com/rust-lang/crates.io-index"
1264 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1265 |
1266 | [[package]]
1267 | name = "winapi-util"
1268 | version = "0.1.11"
1269 | source = "registry+https://github.com/rust-lang/crates.io-index"
1270 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
1271 | dependencies = [
1272 | "windows-sys 0.61.2",
1273 | ]
1274 |
1275 | [[package]]
1276 | name = "winapi-x86_64-pc-windows-gnu"
1277 | version = "0.4.0"
1278 | source = "registry+https://github.com/rust-lang/crates.io-index"
1279 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1280 |
1281 | [[package]]
1282 | name = "windows-link"
1283 | version = "0.2.1"
1284 | source = "registry+https://github.com/rust-lang/crates.io-index"
1285 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
1286 |
1287 | [[package]]
1288 | name = "windows-sys"
1289 | version = "0.60.2"
1290 | source = "registry+https://github.com/rust-lang/crates.io-index"
1291 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
1292 | dependencies = [
1293 | "windows-targets",
1294 | ]
1295 |
1296 | [[package]]
1297 | name = "windows-sys"
1298 | version = "0.61.2"
1299 | source = "registry+https://github.com/rust-lang/crates.io-index"
1300 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
1301 | dependencies = [
1302 | "windows-link",
1303 | ]
1304 |
1305 | [[package]]
1306 | name = "windows-targets"
1307 | version = "0.53.5"
1308 | source = "registry+https://github.com/rust-lang/crates.io-index"
1309 | checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
1310 | dependencies = [
1311 | "windows-link",
1312 | "windows_aarch64_gnullvm",
1313 | "windows_aarch64_msvc",
1314 | "windows_i686_gnu",
1315 | "windows_i686_gnullvm",
1316 | "windows_i686_msvc",
1317 | "windows_x86_64_gnu",
1318 | "windows_x86_64_gnullvm",
1319 | "windows_x86_64_msvc",
1320 | ]
1321 |
1322 | [[package]]
1323 | name = "windows_aarch64_gnullvm"
1324 | version = "0.53.1"
1325 | source = "registry+https://github.com/rust-lang/crates.io-index"
1326 | checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
1327 |
1328 | [[package]]
1329 | name = "windows_aarch64_msvc"
1330 | version = "0.53.1"
1331 | source = "registry+https://github.com/rust-lang/crates.io-index"
1332 | checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
1333 |
1334 | [[package]]
1335 | name = "windows_i686_gnu"
1336 | version = "0.53.1"
1337 | source = "registry+https://github.com/rust-lang/crates.io-index"
1338 | checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
1339 |
1340 | [[package]]
1341 | name = "windows_i686_gnullvm"
1342 | version = "0.53.1"
1343 | source = "registry+https://github.com/rust-lang/crates.io-index"
1344 | checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
1345 |
1346 | [[package]]
1347 | name = "windows_i686_msvc"
1348 | version = "0.53.1"
1349 | source = "registry+https://github.com/rust-lang/crates.io-index"
1350 | checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
1351 |
1352 | [[package]]
1353 | name = "windows_x86_64_gnu"
1354 | version = "0.53.1"
1355 | source = "registry+https://github.com/rust-lang/crates.io-index"
1356 | checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
1357 |
1358 | [[package]]
1359 | name = "windows_x86_64_gnullvm"
1360 | version = "0.53.1"
1361 | source = "registry+https://github.com/rust-lang/crates.io-index"
1362 | checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
1363 |
1364 | [[package]]
1365 | name = "windows_x86_64_msvc"
1366 | version = "0.53.1"
1367 | source = "registry+https://github.com/rust-lang/crates.io-index"
1368 | checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
1369 |
1370 | [[package]]
1371 | name = "wit-bindgen"
1372 | version = "0.46.0"
1373 | source = "registry+https://github.com/rust-lang/crates.io-index"
1374 | checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
1375 |
1376 | [[package]]
1377 | name = "xdg"
1378 | version = "3.0.0"
1379 | source = "registry+https://github.com/rust-lang/crates.io-index"
1380 | checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5"
1381 |
1382 | [[package]]
1383 | name = "zstd"
1384 | version = "0.13.3"
1385 | source = "registry+https://github.com/rust-lang/crates.io-index"
1386 | checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
1387 | dependencies = [
1388 | "zstd-safe",
1389 | ]
1390 |
1391 | [[package]]
1392 | name = "zstd-safe"
1393 | version = "7.2.4"
1394 | source = "registry+https://github.com/rust-lang/crates.io-index"
1395 | checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
1396 | dependencies = [
1397 | "zstd-sys",
1398 | ]
1399 |
1400 | [[package]]
1401 | name = "zstd-sys"
1402 | version = "2.0.16+zstd.1.5.7"
1403 | source = "registry+https://github.com/rust-lang/crates.io-index"
1404 | checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
1405 | dependencies = [
1406 | "cc",
1407 | "pkg-config",
1408 | ]
1409 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "figma-agent"
3 | version = "0.4.2"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | anyhow = "=1.0.100"
8 | axum = "=0.8.6"
9 | fontconfig-parser = "=0.5.8"
10 | harfrust = "=0.3.2"
11 | interp = "=2.1.1"
12 | itertools = "=0.14.0"
13 | jsonc-parser = { version = "=0.27.0", features = ["serde"] }
14 | listenfd = "=1.0.2"
15 | read-fonts = "=0.35.0"
16 | serde = { version = "=1.0.228", features = ["derive"] }
17 | serde_json = "=1.0.145"
18 | skrifa = "=0.37.0"
19 | svg = "=0.18.0"
20 | thiserror = "=2.0.17"
21 | tokio = { version = "=1.48.0", features = ["full"] }
22 | tower = { version = "=0.5.2", features = ["full"] }
23 | tower-http = { version = "=0.6.6", features = ["full"] }
24 | tracing = "=0.1.41"
25 | tracing-subscriber = "=0.3.20"
26 | walkdir = "=2.5.0"
27 | xdg = "=3.0.0"
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Hikari Hayashi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Figma Agent for Linux
2 |
3 | [](https://github.com/neetly/figma-agent-linux/actions/workflows/ci.yml)
4 |
5 | This service allows you to use your locally installed fonts on [figma.com](https://www.figma.com/).
6 |
7 | ## Features
8 |
9 | - Variable fonts support
10 | - Preview fonts in the font picker
11 |
12 | ## Installation
13 |
14 | > [!IMPORTANT]
15 | > To make this service work, you need to override the browser's user agent to a Windows one. See [this thread](https://forum.figma.com/report-a-problem-6/requests-to-font-helper-on-linux-stopped-working-16569) for more information.
16 |
17 | ```sh
18 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/neetly/figma-agent-linux/main/files/install.sh)"
19 | ```
20 |
21 | > [!TIP]
22 | > You can run the command again to update this service to the latest version.
23 |
24 | ### Package Managers
25 |
26 | | Package Manager | Package |
27 | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28 | | Arch Linux | [figma-agent-linux](https://aur.archlinux.org/packages/figma-agent-linux) / [figma-agent-linux-bin](https://aur.archlinux.org/packages/figma-agent-linux-bin) |
29 | | Nix | [figma-agent](https://search.nixos.org/packages?show=figma-agent) |
30 |
31 | ### Uninstallation
32 |
33 |
34 |
35 | ```sh
36 | systemctl --user disable --now figma-agent.{service,socket}
37 | rm -rf ~/.local/share/figma-agent ~/.local/share/systemd/user/figma-agent.{service,socket}
38 | ```
39 |
40 |
41 |
42 | ## Configuration
43 |
44 | ```jsonc
45 | // ~/.config/figma-agent/config.json
46 | {
47 | // Default: "127.0.0.1:44950"
48 | "bind": "127.0.0.1:44950",
49 | // Default: true
50 | "use_system_fonts": true,
51 | // Default: []
52 | "font_directories": ["~/Fonts"],
53 | // Default: true
54 | "enable_font_rescan": true,
55 | // Default: true
56 | "enable_font_preview": true,
57 | }
58 | ```
59 |
60 | > [!NOTE]
61 | > You need to restart this service to apply the configuration changes.
62 | >
63 | > ```sh
64 | > systemctl --user restart figma-agent.service
65 | > ```
66 |
67 | ## Caveats
68 |
69 | ### Ad Blockers
70 |
71 | Ad blockers may prevent websites from connecting to localhost for privacy concerns. Please disable the relevant rules or create an exception rule for [figma.com](https://www.figma.com/).
72 |
73 | ### Brave Browser
74 |
75 | In Brave browser, websites require special permissions to access localhost. Please follow the instructions in [the documentation](https://brave.com/privacy-updates/27-localhost-permission/) to grant the permission to [figma.com](https://www.figma.com/).
76 |
77 | ## Credits
78 |
79 | This project is inspired by [Figma Linux Font Helper](https://github.com/Figma-Linux/figma-linux-font-helper).
80 |
--------------------------------------------------------------------------------
/files/figma-agent.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Figma Agent for Linux Service
3 | Requires=figma-agent.socket
4 |
5 | [Service]
6 | Type=exec
7 | ExecStart=/usr/bin/figma-agent
8 |
9 | [Install]
10 | WantedBy=default.target
11 |
--------------------------------------------------------------------------------
/files/figma-agent.socket:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Figma Agent for Linux Socket
3 |
4 | [Socket]
5 | ListenStream=127.0.0.1:44950
6 |
7 | [Install]
8 | WantedBy=sockets.target
9 |
--------------------------------------------------------------------------------
/files/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 |
4 | XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
5 |
6 | echo ":: Figma Agent for Linux"
7 | echo
8 |
9 | echo ":: Downloading figma-agent..."
10 | TMPFILE="$(mktemp)"
11 | curl --fail --location "https://github.com/neetly/figma-agent-linux/releases/latest/download/figma-agent-$(uname -m)-unknown-linux-gnu" \
12 | --output "$TMPFILE"
13 | chmod +x "$TMPFILE"
14 | mkdir -p "$XDG_DATA_HOME/figma-agent"
15 | mv "$TMPFILE" "$XDG_DATA_HOME/figma-agent/figma-agent"
16 | echo
17 |
18 | echo ":: Creating figma-agent.service and figma-agent.socket..."
19 | mkdir -p "$XDG_DATA_HOME/systemd/user"
20 | cat > "$XDG_DATA_HOME/systemd/user/figma-agent.service" << EOF
21 | [Unit]
22 | Description=Figma Agent for Linux Service
23 | Requires=figma-agent.socket
24 |
25 | [Service]
26 | Type=exec
27 | ExecStart="$XDG_DATA_HOME/figma-agent/figma-agent"
28 |
29 | [Install]
30 | WantedBy=default.target
31 | EOF
32 | cat > "$XDG_DATA_HOME/systemd/user/figma-agent.socket" << EOF
33 | [Unit]
34 | Description=Figma Agent for Linux Socket
35 |
36 | [Socket]
37 | ListenStream=127.0.0.1:44950
38 |
39 | [Install]
40 | WantedBy=sockets.target
41 | EOF
42 | echo
43 |
44 | echo ":: Enabling figma-agent.socket..."
45 | systemctl --user daemon-reload
46 | systemctl --user enable --now figma-agent.socket
47 | echo
48 |
49 | if systemctl --user is-active figma-agent.service > /dev/null; then
50 | echo ":: Restarting figma-agent.service..."
51 | systemctl --user restart figma-agent.service
52 | echo
53 | fi
54 |
55 | echo ":: Done"
56 |
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.90.0"
3 | targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"]
4 | profile = "default"
5 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | use_field_init_shorthand = true
2 | unstable_features = true
3 | group_imports = "StdExternalCrate"
4 | imports_granularity = "Crate"
5 |
--------------------------------------------------------------------------------
/src/config.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fs, io, iter,
3 | path::{Path, PathBuf},
4 | };
5 |
6 | use fontconfig_parser::FontConfig;
7 | use itertools::{Either, Itertools};
8 |
9 | use crate::path::expand_home;
10 |
11 | #[derive(Debug, thiserror::Error)]
12 | pub enum ConfigError {
13 | #[error("Failed to read config file")]
14 | Read(#[from] io::Error),
15 | #[error("Failed to parse config file")]
16 | Parse(#[from] jsonc_parser::errors::ParseError),
17 | #[error("Failed to parse config file")]
18 | Deserialize(#[from] serde_json::Error),
19 | #[error("Failed to parse config file")]
20 | Invalid,
21 | }
22 |
23 | #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize)]
24 | pub struct Config {
25 | #[serde(default = "default_bind")]
26 | pub bind: String,
27 | #[serde(default = "default_bool::")]
28 | pub use_system_fonts: bool,
29 | #[serde(default)]
30 | pub font_directories: Vec,
31 | #[serde(default = "default_bool::")]
32 | pub enable_font_rescan: bool,
33 | #[serde(default = "default_bool::")]
34 | pub enable_font_preview: bool,
35 | }
36 |
37 | fn default_bind() -> String {
38 | "127.0.0.1:44950".into()
39 | }
40 |
41 | fn default_bool() -> bool {
42 | V
43 | }
44 |
45 | impl Default for Config {
46 | fn default() -> Self {
47 | serde_json::from_value(serde_json::json!({})).unwrap()
48 | }
49 | }
50 |
51 | impl Config {
52 | pub fn parse(text: impl AsRef) -> Result {
53 | let value = jsonc_parser::parse_to_serde_value(
54 | text.as_ref(),
55 | &jsonc_parser::ParseOptions::default(),
56 | )?;
57 | let value = value.ok_or(ConfigError::Invalid)?;
58 | let config = serde_json::from_value(value)?;
59 | Ok(config)
60 | }
61 |
62 | pub fn from_path(path: impl AsRef) -> Result {
63 | let text = fs::read_to_string(path)?;
64 | Config::parse(text)
65 | }
66 | }
67 |
68 | #[test]
69 | fn test_default() {
70 | assert_eq!(
71 | Config::default(),
72 | Config {
73 | bind: "127.0.0.1:44950".into(),
74 | use_system_fonts: true,
75 | font_directories: vec![],
76 | enable_font_rescan: true,
77 | enable_font_preview: true,
78 | },
79 | );
80 | }
81 |
82 | #[test]
83 | fn test_parse() {
84 | assert_eq!(Config::parse("{}").unwrap(), Config::default());
85 | assert_eq!(Config::parse("{} // comment").unwrap(), Config::default());
86 | assert_eq!(
87 | Config::parse(
88 | r#"{ "bind": "0.0.0.0:44950", "use_system_fonts": false, "font_directories": ["/usr/share/fonts"], "enable_font_rescan": false, "enable_font_preview": false }"#,
89 | )
90 | .unwrap(),
91 | Config {
92 | bind: "0.0.0.0:44950".into(),
93 | use_system_fonts: false,
94 | font_directories: vec![PathBuf::from("/usr/share/fonts")],
95 | enable_font_rescan: false,
96 | enable_font_preview: false,
97 | },
98 | );
99 | }
100 |
101 | impl Config {
102 | pub fn effective_font_directories(
103 | &self,
104 | fontconfig: &FontConfig,
105 | ) -> impl Iterator- {
106 | self.font_directories
107 | .iter()
108 | .filter_map(|directory| match expand_home(directory) {
109 | Ok(directory) => Some(directory),
110 | Err(error) => {
111 | tracing::debug!("Skipped font directory: {directory:?}, error: {error:?}");
112 | None
113 | }
114 | })
115 | .chain(if self.use_system_fonts {
116 | Either::Left(fontconfig.dirs.iter().map(|dir| dir.path.clone()))
117 | } else {
118 | Either::Right(iter::empty())
119 | })
120 | .filter_map(|directory| match directory.canonicalize() {
121 | Ok(directory) => Some(directory),
122 | Err(error) => {
123 | tracing::debug!("Skipped font directory: {directory:?}, error: {error:?}");
124 | None
125 | }
126 | })
127 | .unique()
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/font.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fs,
3 | path::{Path, PathBuf},
4 | time::SystemTime,
5 | };
6 |
7 | use interp::{InterpMode, interp};
8 | use skrifa::{MetadataProvider, string::StringId};
9 |
10 | #[derive(Debug, thiserror::Error)]
11 | pub enum FontError {
12 | #[error("Failed to read font file")]
13 | Read(#[from] std::io::Error),
14 | #[error("Failed to parse font file")]
15 | Parse(Vec<(usize, read_fonts::ReadError)>, Option),
16 | }
17 |
18 | #[derive(Debug, Clone)]
19 | pub struct FontFile {
20 | pub path: PathBuf,
21 | pub fonts: Vec,
22 | pub modified_at: Option,
23 | }
24 |
25 | impl FontFile {
26 | pub fn from_path(path: impl AsRef) -> Result {
27 | let path = path.as_ref();
28 |
29 | let data = fs::read(path)?;
30 | let metadata = fs::metadata(path)?;
31 |
32 | let mut errors = Vec::new();
33 | let fonts = skrifa::FontRef::fonts(&data)
34 | .enumerate()
35 | .filter_map(|(index, font)| match font {
36 | Ok(font) => Some(Font::from(&font, index)),
37 | Err(error) => {
38 | errors.push((index, error));
39 | None
40 | }
41 | })
42 | .collect();
43 |
44 | let font_file = FontFile {
45 | path: path.into(),
46 | fonts,
47 | modified_at: metadata.modified().ok(),
48 | };
49 |
50 | if errors.is_empty() {
51 | Ok(font_file)
52 | } else {
53 | Err(FontError::Parse(errors, Some(font_file)))
54 | }
55 | }
56 | }
57 |
58 | #[derive(Debug, Clone)]
59 | pub struct FontQuery<'a> {
60 | pub family_name: Option<&'a str>,
61 | pub subfamily_name: Option<&'a str>,
62 | pub postscript_name: Option<&'a str>,
63 | }
64 |
65 | #[derive(Debug, Clone)]
66 | pub struct FontQueryResult<'a> {
67 | pub font: &'a Font,
68 | pub named_instance: Option<&'a NamedInstance>,
69 | }
70 |
71 | impl<'a> FontFile {
72 | pub fn query(&'a self, query: FontQuery<'_>) -> Option> {
73 | fn matches(value: &Option>, query: &Option>) -> bool {
74 | match (value, query) {
75 | (Some(value), Some(query)) => value.as_ref() == query.as_ref(),
76 | (None, Some(_)) => false,
77 | (_, None) => true,
78 | }
79 | }
80 |
81 | self.fonts
82 | .iter()
83 | .filter_map(|font| {
84 | if matches(&font.family_name, &query.family_name) {
85 | if matches(&font.subfamily_name, &query.subfamily_name)
86 | && matches(&font.postscript_name, &query.postscript_name)
87 | {
88 | Some(FontQueryResult {
89 | font,
90 | named_instance: None,
91 | })
92 | } else {
93 | font.named_instances
94 | .iter()
95 | .filter_map(|named_instance| {
96 | if matches(&named_instance.subfamily_name, &query.subfamily_name)
97 | && matches(
98 | &named_instance.postscript_name,
99 | &query.postscript_name,
100 | )
101 | {
102 | Some(FontQueryResult {
103 | font,
104 | named_instance: Some(named_instance),
105 | })
106 | } else {
107 | None
108 | }
109 | })
110 | .next()
111 | }
112 | } else {
113 | None
114 | }
115 | })
116 | .next()
117 | }
118 | }
119 |
120 | #[derive(Debug, Clone)]
121 | pub struct Font {
122 | pub index: usize,
123 | pub family_name: Option,
124 | pub subfamily_name: Option,
125 | pub postscript_name: Option,
126 | pub weight: f32,
127 | pub width: f32,
128 | pub is_italic: bool,
129 | pub is_oblique: bool,
130 | pub axes: Vec,
131 | pub named_instances: Vec,
132 | }
133 |
134 | impl Font {
135 | pub fn from(font: &skrifa::FontRef, index: usize) -> Self {
136 | let attributes = font.attributes();
137 |
138 | Font {
139 | index,
140 | family_name: font
141 | .string(StringId::TYPOGRAPHIC_FAMILY_NAME)
142 | .or_else(|| font.string(StringId::FAMILY_NAME)),
143 | subfamily_name: font
144 | .string(StringId::TYPOGRAPHIC_SUBFAMILY_NAME)
145 | .or_else(|| font.string(StringId::SUBFAMILY_NAME)),
146 | postscript_name: font.string(StringId::POSTSCRIPT_NAME),
147 | weight: attributes.weight.value(),
148 | width: attributes.stretch.percentage(),
149 | is_italic: matches!(attributes.style, skrifa::attribute::Style::Italic),
150 | is_oblique: matches!(attributes.style, skrifa::attribute::Style::Oblique(_)),
151 | axes: font
152 | .axes()
153 | .iter()
154 | .enumerate()
155 | .map(|(index, axis)| Axis::from(font, &axis, index))
156 | .collect(),
157 | named_instances: font
158 | .named_instances()
159 | .iter()
160 | .enumerate()
161 | .map(|(index, named_instance)| NamedInstance::from(font, &named_instance, index))
162 | .collect(),
163 | }
164 | }
165 | }
166 |
167 | #[derive(Debug, Clone)]
168 | pub struct Axis {
169 | pub index: usize,
170 | pub tag: String,
171 | pub name: Option,
172 | pub min_value: f32,
173 | pub max_value: f32,
174 | pub default_value: f32,
175 | pub is_hidden: bool,
176 | }
177 |
178 | impl Axis {
179 | pub fn from(font: &skrifa::FontRef, axis: &skrifa::Axis, index: usize) -> Self {
180 | Axis {
181 | index,
182 | tag: axis.tag().to_string(),
183 | name: font.string(axis.name_id()),
184 | min_value: axis.min_value(),
185 | max_value: axis.max_value(),
186 | default_value: axis.default_value(),
187 | is_hidden: axis.is_hidden(),
188 | }
189 | }
190 | }
191 |
192 | #[derive(Debug, Clone)]
193 | pub struct NamedInstance {
194 | pub index: usize,
195 | pub subfamily_name: Option,
196 | pub postscript_name: Option,
197 | pub coordinates: Vec,
198 | }
199 |
200 | impl NamedInstance {
201 | pub fn from(
202 | font: &skrifa::FontRef,
203 | named_instance: &skrifa::NamedInstance,
204 | index: usize,
205 | ) -> Self {
206 | NamedInstance {
207 | index,
208 | subfamily_name: font.string(named_instance.subfamily_name_id()),
209 | postscript_name: named_instance
210 | .postscript_name_id()
211 | .and_then(|id| font.string(id))
212 | .or_else(|| {
213 | // https://adobe-type-tools.github.io/font-tech-notes/pdfs/5902.AdobePSNameGeneration.pdf
214 | font.string(StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX)
215 | .or_else(|| {
216 | font.string(StringId::TYPOGRAPHIC_FAMILY_NAME)
217 | .map(|family_name| family_name.postscript())
218 | })
219 | .zip(font.string(named_instance.subfamily_name_id()))
220 | .map(|(postscript_family_prefix, subfamily)| {
221 | format!("{}-{}", postscript_family_prefix, subfamily.postscript())
222 | })
223 | }),
224 | coordinates: named_instance.user_coords().collect(),
225 | }
226 | }
227 | }
228 |
229 | pub trait StringExt {
230 | fn postscript(&self) -> String;
231 | }
232 |
233 | impl StringExt for String {
234 | fn postscript(&self) -> String {
235 | self.chars()
236 | .filter(|char| char.is_ascii_alphanumeric())
237 | .collect()
238 | }
239 | }
240 |
241 | pub trait SkrifaFontRefExt {
242 | fn string(&self, id: StringId) -> Option;
243 | }
244 |
245 | impl SkrifaFontRefExt for skrifa::FontRef<'_> {
246 | fn string(&self, id: StringId) -> Option {
247 | self.localized_strings(id)
248 | .english_or_first()
249 | .map(|localized_string| localized_string.to_string())
250 | }
251 | }
252 |
253 | /// Convert weight axis (wght) to OS/2 usWeightClass.
254 | ///
255 | /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass
256 | pub fn to_us_weight_class(weight: f32) -> u16 {
257 | weight.round() as u16
258 | }
259 |
260 | /// Convert width axis (wdth) to OS/2 usWidthClass.
261 | ///
262 | /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass
263 | pub fn to_us_width_class(width: f32) -> u16 {
264 | static WIDTH_VALUES: [f32; 9] = [50.0, 62.5, 75.0, 87.5, 100.0, 112.5, 125.0, 150.0, 200.0];
265 | static US_WIDTH_CLASS_VALUES: [f32; 9] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
266 |
267 | interp(
268 | &WIDTH_VALUES,
269 | &US_WIDTH_CLASS_VALUES,
270 | width,
271 | &InterpMode::FirstLast,
272 | )
273 | .round() as u16
274 | }
275 |
276 | #[test]
277 | fn test_to_us_width_class() {
278 | assert_eq!(to_us_width_class(50.0), 1);
279 | assert_eq!(to_us_width_class(62.5), 2);
280 | assert_eq!(to_us_width_class(75.0), 3);
281 | assert_eq!(to_us_width_class(87.5), 4);
282 | assert_eq!(to_us_width_class(100.0), 5);
283 | assert_eq!(to_us_width_class(112.5), 6);
284 | assert_eq!(to_us_width_class(125.0), 7);
285 | assert_eq!(to_us_width_class(150.0), 8);
286 | assert_eq!(to_us_width_class(200.0), 9);
287 | }
288 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | collections::{HashMap, HashSet},
3 | fs,
4 | path::{Path, PathBuf},
5 | sync::LazyLock,
6 | };
7 |
8 | use fontconfig_parser::FontConfig;
9 | use tokio::sync::RwLock;
10 |
11 | use crate::{
12 | config::Config,
13 | font::{FontError, FontFile},
14 | scanner::scan_font_paths,
15 | };
16 |
17 | pub mod config;
18 | pub mod font;
19 | pub mod path;
20 | pub mod payload;
21 | pub mod renderer;
22 | pub mod routes;
23 | pub mod scanner;
24 |
25 | pub static XDG_DIRECTORIES: LazyLock =
26 | LazyLock::new(|| xdg::BaseDirectories::with_prefix("figma-agent"));
27 |
28 | pub static FONTCONFIG: LazyLock = LazyLock::new(|| {
29 | let mut font_config = FontConfig::default();
30 | if let Err(error) = font_config.merge_config("/etc/fonts/fonts.conf") {
31 | tracing::warn!(
32 | "Failed to load Fontconfig config file: /etc/fonts/fonts.conf, error: {error:?}"
33 | );
34 | }
35 | font_config
36 | });
37 |
38 | pub static CONFIG: LazyLock = LazyLock::new(|| {
39 | XDG_DIRECTORIES
40 | .find_config_file("config.json")
41 | .and_then(|path| {
42 | tracing::info!("Use config file: {path:?}");
43 | match Config::from_path(&path) {
44 | Ok(config) => {
45 | tracing::info!("Use config: {config:?}");
46 | Some(config)
47 | }
48 | Err(error) => {
49 | tracing::error!("Failed to load config file: {path:?}, error: {error:?}");
50 | None
51 | }
52 | }
53 | })
54 | .unwrap_or_else(|| {
55 | let config = Config::default();
56 | tracing::info!("Use default config: {config:?}");
57 | config
58 | })
59 | });
60 |
61 | pub static EFFECTIVE_FONT_DIRECTORIES: LazyLock> = LazyLock::new(|| {
62 | let directories = CONFIG.effective_font_directories(&FONTCONFIG).collect();
63 | tracing::info!("Use effective font directories: {directories:?}");
64 | directories
65 | });
66 |
67 | pub static FONT_FILES: LazyLock>> =
68 | LazyLock::new(|| RwLock::new(HashMap::new()));
69 |
70 | #[tracing::instrument]
71 | pub async fn scan_font_files() {
72 | tracing::debug!("Scanning font files...");
73 |
74 | let mut font_files = FONT_FILES.write().await;
75 |
76 | let (mut added_count, mut updated_count, mut removed_count) = (0, 0, 0);
77 | let mut font_paths = scan_font_paths(&*EFFECTIVE_FONT_DIRECTORIES).collect::>();
78 |
79 | font_files.retain(|path, _| {
80 | let contains = font_paths.contains(path);
81 | if !contains {
82 | removed_count += 1;
83 | }
84 | contains
85 | });
86 |
87 | font_paths.retain(|path| {
88 | if let Some(font_file) = font_files.get(path) {
89 | let modified_at = fs::metadata(path)
90 | .and_then(|metadata| metadata.modified())
91 | .ok();
92 | modified_at > font_file.modified_at
93 | } else {
94 | true
95 | }
96 | });
97 |
98 | for path in font_paths {
99 | if let Some(font_file) = load_font_file(&path) {
100 | if font_files.insert(path, font_file).is_none() {
101 | added_count += 1;
102 | } else {
103 | updated_count += 1;
104 | }
105 | }
106 | }
107 |
108 | tracing::debug!(
109 | "{count} font files loaded ({added_count} added, {updated_count} updated, {removed_count} removed)",
110 | count = font_files.len(),
111 | );
112 | }
113 |
114 | pub fn load_font_file(path: impl AsRef) -> Option {
115 | let path = path.as_ref();
116 |
117 | match FontFile::from_path(path) {
118 | Ok(font_file) => Some(font_file),
119 | Err(FontError::Read(error)) => {
120 | tracing::debug!("Failed to load font file: {path:?}, error: {error:?}");
121 | None
122 | }
123 | Err(FontError::Parse(errors, font_file)) => {
124 | for (index, error) in errors {
125 | tracing::debug!("Failed to load font file: {path:?} ({index}), error: {error:?}",);
126 | }
127 | font_file
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::sync::LazyLock;
2 |
3 | use axum::{Router, http::HeaderValue, routing::get};
4 | use figma_agent::{CONFIG, EFFECTIVE_FONT_DIRECTORIES, routes, scan_font_files};
5 | use listenfd::ListenFd;
6 | use tokio::net::TcpListener;
7 | use tower::ServiceBuilder;
8 | use tower_http::{cors::CorsLayer, trace::TraceLayer};
9 |
10 | #[tokio::main]
11 | async fn main() -> Result<(), anyhow::Error> {
12 | tracing_subscriber::fmt::init();
13 |
14 | LazyLock::force(&CONFIG);
15 | LazyLock::force(&EFFECTIVE_FONT_DIRECTORIES);
16 |
17 | scan_font_files().await;
18 |
19 | let app = Router::new()
20 | .route("/figma/font-files", get(routes::font_files))
21 | .route("/figma/font-file", get(routes::font_file))
22 | .route("/figma/font-preview", get(routes::font_preview))
23 | .layer(
24 | ServiceBuilder::new()
25 | .layer(TraceLayer::new_for_http())
26 | .layer(
27 | CorsLayer::new()
28 | .allow_origin(HeaderValue::from_static("https://www.figma.com"))
29 | .allow_private_network(true),
30 | ),
31 | );
32 |
33 | let listener = match ListenFd::from_env().take_tcp_listener(0)? {
34 | Some(listener) => {
35 | listener.set_nonblocking(true)?;
36 | TcpListener::from_std(listener)?
37 | }
38 | None => TcpListener::bind(&CONFIG.bind).await?,
39 | };
40 | tracing::info!("Listening on {}", listener.local_addr()?);
41 |
42 | axum::serve(listener, app).await?;
43 |
44 | Ok(())
45 | }
46 |
--------------------------------------------------------------------------------
/src/path.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | env,
3 | path::{Path, PathBuf},
4 | };
5 |
6 | #[derive(Debug, thiserror::Error)]
7 | pub enum PathError {
8 | #[error("Failed to get home directory")]
9 | HomeNotDefined,
10 | }
11 |
12 | pub fn expand_home(path: impl AsRef) -> Result {
13 | let path = path.as_ref();
14 |
15 | if let Ok(path_relative_to_home) = path.strip_prefix("~") {
16 | env::home_dir()
17 | .map(|home_directory| home_directory.join(path_relative_to_home))
18 | .ok_or(PathError::HomeNotDefined)
19 | } else {
20 | Ok(path.into())
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/payload.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashMap, path::PathBuf};
2 |
3 | #[derive(Debug, Clone, serde::Serialize)]
4 | pub struct FontFilesEndpointPayload {
5 | #[serde(rename = "fontFiles")]
6 | pub font_files: HashMap>,
7 | pub modified_at: Option,
8 | pub modified_fonts: Option>>,
9 | pub package: String,
10 | pub version: u32,
11 | }
12 |
13 | #[derive(Debug, Clone, serde::Serialize)]
14 | pub struct FontPayload {
15 | pub family: String, // Family name
16 | pub style: String, // Subfamily name
17 | pub postscript: String, // PostScript name
18 | pub weight: u16, // Weight (OS/2 usWeightClass)
19 | pub stretch: u16, // Width (OS/2 usWidthClass)
20 | pub italic: bool,
21 | #[serde(rename = "variationAxes", skip_serializing_if = "Option::is_none")]
22 | pub variation_axes: Option>,
23 | pub modified_at: u64,
24 | pub user_installed: bool,
25 | }
26 |
27 | #[derive(Debug, Clone, serde::Serialize)]
28 | pub struct VariationAxisPayload {
29 | pub tag: String,
30 | pub name: String,
31 | pub value: f32,
32 | pub min: f32,
33 | pub max: f32,
34 | pub default: f32,
35 | pub hidden: bool,
36 | }
37 |
--------------------------------------------------------------------------------
/src/renderer.rs:
--------------------------------------------------------------------------------
1 | use std::{fs, iter, path::Path};
2 |
3 | use harfrust::{ShaperData, ShaperInstance, UnicodeBuffer};
4 | use skrifa::{
5 | FontRef, GlyphId, MetadataProvider,
6 | instance::Size,
7 | outline::{DrawError, DrawSettings, OutlinePen},
8 | };
9 | use svg::{
10 | Document,
11 | node::element::{
12 | self,
13 | path::{Command, Position},
14 | },
15 | };
16 |
17 | #[derive(Debug, thiserror::Error)]
18 | pub enum RenderError {
19 | #[error("Failed to read font file")]
20 | Read(#[from] std::io::Error),
21 | #[error("Failed to parse font file")]
22 | Parse(#[from] read_fonts::ReadError),
23 | #[error("Failed to draw glyph")]
24 | Draw(#[from] DrawError),
25 | }
26 |
27 | #[derive(Debug, Clone)]
28 | pub struct RenderOptions<'a> {
29 | pub font: (&'a Path, usize),
30 | pub size: f32,
31 | pub named_instance_index: Option,
32 | }
33 |
34 | pub fn render_text(
35 | text: impl AsRef,
36 | RenderOptions {
37 | font: (font_path, font_index),
38 | size,
39 | named_instance_index,
40 | }: RenderOptions,
41 | ) -> Result