git.iko.soy/rowtype-yoga/purescript-tauri
on main help sign in
15 files changed+2903−100
since splitsnapshots
unifiedsplit
native/runtime/Cargo.lock+1113 native/runtime/Cargo.toml+3−1 native/runtime/src/graphics.rs+64−61 native/runtime/src/jsc.rs+1293 native/runtime/src/lib.rs+232−23 native/runtime/src/terminal.rs+45 native/runtime/src/terminal_shm.rs+69−8 src/Tauri/Native.js+1 src/Tauri/Native.purs+8−1 src/Tauri/Native/Canvas.js+1−1 src/Tauri/Native/Canvas.purs+11−3 src/Tauri/Native/SharedFrame.js+12 src/Tauri/Native/SharedFrame.purs+41 src/Tauri/Native/TerminalGraphics.js+1 src/Tauri/Native/TerminalGraphics.purs+9−2
native/runtime/Cargo.locknew file+1113
@@ -0,0 +1,1113 @@
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 = "aho-corasick"
13+version = "1.1.5"
14+source = "registry+https://github.com/rust-lang/crates.io-index"
15+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
16+dependencies = [
17+ "memchr",
18+]
19+
20+[[package]]
21+name = "arrayvec"
22+version = "0.7.8"
23+source = "registry+https://github.com/rust-lang/crates.io-index"
24+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
25+
26+[[package]]
27+name = "autocfg"
28+version = "1.5.1"
29+source = "registry+https://github.com/rust-lang/crates.io-index"
30+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
31+
32+[[package]]
33+name = "base64"
34+version = "0.22.1"
35+source = "registry+https://github.com/rust-lang/crates.io-index"
36+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
37+
38+[[package]]
39+name = "bindgen"
40+version = "0.72.1"
41+source = "registry+https://github.com/rust-lang/crates.io-index"
42+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
43+dependencies = [
44+ "bitflags",
45+ "cexpr",
46+ "clang-sys",
47+ "itertools",
48+ "log",
49+ "prettyplease",
50+ "proc-macro2",
51+ "quote",
52+ "regex",
53+ "rustc-hash",
54+ "shlex 1.3.0",
55+ "syn 2.0.119",
56+]
57+
58+[[package]]
59+name = "bitflags"
60+version = "2.13.2"
61+source = "registry+https://github.com/rust-lang/crates.io-index"
62+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
63+
64+[[package]]
65+name = "block2"
66+version = "0.6.2"
67+source = "registry+https://github.com/rust-lang/crates.io-index"
68+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
69+dependencies = [
70+ "objc2",
71+]
72+
73+[[package]]
74+name = "bytemuck"
75+version = "1.25.2"
76+source = "registry+https://github.com/rust-lang/crates.io-index"
77+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
78+
79+[[package]]
80+name = "cc"
81+version = "1.6.0"
82+source = "registry+https://github.com/rust-lang/crates.io-index"
83+checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
84+dependencies = [
85+ "find-msvc-tools",
86+ "shlex 2.0.1",
87+]
88+
89+[[package]]
90+name = "cexpr"
91+version = "0.6.0"
92+source = "registry+https://github.com/rust-lang/crates.io-index"
93+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
94+dependencies = [
95+ "nom",
96+]
97+
98+[[package]]
99+name = "cfg-if"
100+version = "1.0.5"
101+source = "registry+https://github.com/rust-lang/crates.io-index"
102+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
103+
104+[[package]]
105+name = "cfg_aliases"
106+version = "0.2.2"
107+source = "registry+https://github.com/rust-lang/crates.io-index"
108+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
109+
110+[[package]]
111+name = "clang-sys"
112+version = "1.9.1"
113+source = "registry+https://github.com/rust-lang/crates.io-index"
114+checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
115+dependencies = [
116+ "glob",
117+ "libc",
118+ "libloading",
119+]
120+
121+[[package]]
122+name = "core_maths"
123+version = "0.1.1"
124+source = "registry+https://github.com/rust-lang/crates.io-index"
125+checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
126+dependencies = [
127+ "libm",
128+]
129+
130+[[package]]
131+name = "crc32fast"
132+version = "1.5.2"
133+source = "registry+https://github.com/rust-lang/crates.io-index"
134+checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
135+dependencies = [
136+ "cfg-if",
137+]
138+
139+[[package]]
140+name = "crossterm"
141+version = "0.28.1"
142+source = "registry+https://github.com/rust-lang/crates.io-index"
143+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
144+dependencies = [
145+ "bitflags",
146+ "crossterm_winapi",
147+ "mio",
148+ "parking_lot",
149+ "rustix 0.38.44",
150+ "signal-hook",
151+ "signal-hook-mio",
152+ "winapi",
153+]
154+
155+[[package]]
156+name = "crossterm_winapi"
157+version = "0.9.1"
158+source = "registry+https://github.com/rust-lang/crates.io-index"
159+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
160+dependencies = [
161+ "winapi",
162+]
163+
164+[[package]]
165+name = "ctrlc"
166+version = "3.5.2"
167+source = "registry+https://github.com/rust-lang/crates.io-index"
168+checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
169+dependencies = [
170+ "dispatch2",
171+ "nix",
172+ "windows-sys 0.61.2",
173+]
174+
175+[[package]]
176+name = "dispatch2"
177+version = "0.3.1"
178+source = "registry+https://github.com/rust-lang/crates.io-index"
179+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
180+dependencies = [
181+ "bitflags",
182+ "block2",
183+ "libc",
184+ "objc2",
185+]
186+
187+[[package]]
188+name = "either"
189+version = "1.19.0"
190+source = "registry+https://github.com/rust-lang/crates.io-index"
191+checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
192+
193+[[package]]
194+name = "equivalent"
195+version = "1.0.2"
196+source = "registry+https://github.com/rust-lang/crates.io-index"
197+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
198+
199+[[package]]
200+name = "errno"
201+version = "0.3.14"
202+source = "registry+https://github.com/rust-lang/crates.io-index"
203+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
204+dependencies = [
205+ "libc",
206+ "windows-sys 0.61.2",
207+]
208+
209+[[package]]
210+name = "euclid"
211+version = "0.22.14"
212+source = "registry+https://github.com/rust-lang/crates.io-index"
213+checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"
214+dependencies = [
215+ "num-traits",
216+]
217+
218+[[package]]
219+name = "filetime"
220+version = "0.2.29"
221+source = "registry+https://github.com/rust-lang/crates.io-index"
222+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
223+dependencies = [
224+ "cfg-if",
225+ "libc",
226+]
227+
228+[[package]]
229+name = "find-msvc-tools"
230+version = "0.1.14"
231+source = "registry+https://github.com/rust-lang/crates.io-index"
232+checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
233+
234+[[package]]
235+name = "flate2"
236+version = "1.1.10"
237+source = "registry+https://github.com/rust-lang/crates.io-index"
238+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
239+dependencies = [
240+ "crc32fast",
241+ "miniz_oxide",
242+ "zlib-rs",
243+]
244+
245+[[package]]
246+name = "fontconfig-parser"
247+version = "0.5.8"
248+source = "registry+https://github.com/rust-lang/crates.io-index"
249+checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
250+dependencies = [
251+ "roxmltree",
252+]
253+
254+[[package]]
255+name = "fontdb"
256+version = "0.23.0"
257+source = "registry+https://github.com/rust-lang/crates.io-index"
258+checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
259+dependencies = [
260+ "fontconfig-parser",
261+ "log",
262+ "memmap2",
263+ "slotmap",
264+ "tinyvec",
265+ "ttf-parser",
266+]
267+
268+[[package]]
269+name = "glob"
270+version = "0.3.4"
271+source = "registry+https://github.com/rust-lang/crates.io-index"
272+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
273+
274+[[package]]
275+name = "hashbrown"
276+version = "0.17.1"
277+source = "registry+https://github.com/rust-lang/crates.io-index"
278+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
279+
280+[[package]]
281+name = "heck"
282+version = "0.5.0"
283+source = "registry+https://github.com/rust-lang/crates.io-index"
284+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
285+
286+[[package]]
287+name = "indexmap"
288+version = "2.14.2"
289+source = "registry+https://github.com/rust-lang/crates.io-index"
290+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
291+dependencies = [
292+ "equivalent",
293+ "hashbrown",
294+]
295+
296+[[package]]
297+name = "itertools"
298+version = "0.13.0"
299+source = "registry+https://github.com/rust-lang/crates.io-index"
300+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
301+dependencies = [
302+ "either",
303+]
304+
305+[[package]]
306+name = "itoa"
307+version = "1.0.18"
308+source = "registry+https://github.com/rust-lang/crates.io-index"
309+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
310+
311+[[package]]
312+name = "kurbo"
313+version = "0.11.3"
314+source = "registry+https://github.com/rust-lang/crates.io-index"
315+checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"
316+dependencies = [
317+ "arrayvec",
318+ "euclid",
319+ "smallvec",
320+]
321+
322+[[package]]
323+name = "libc"
324+version = "0.2.190"
325+source = "registry+https://github.com/rust-lang/crates.io-index"
326+checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
327+
328+[[package]]
329+name = "libloading"
330+version = "0.8.9"
331+source = "registry+https://github.com/rust-lang/crates.io-index"
332+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
333+dependencies = [
334+ "cfg-if",
335+ "windows-link",
336+]
337+
338+[[package]]
339+name = "libm"
340+version = "0.2.16"
341+source = "registry+https://github.com/rust-lang/crates.io-index"
342+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
343+
344+[[package]]
345+name = "linux-raw-sys"
346+version = "0.4.15"
347+source = "registry+https://github.com/rust-lang/crates.io-index"
348+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
349+
350+[[package]]
351+name = "linux-raw-sys"
352+version = "0.12.1"
353+source = "registry+https://github.com/rust-lang/crates.io-index"
354+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
355+
356+[[package]]
357+name = "lock_api"
358+version = "0.4.14"
359+source = "registry+https://github.com/rust-lang/crates.io-index"
360+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
361+dependencies = [
362+ "scopeguard",
363+]
364+
365+[[package]]
366+name = "log"
367+version = "0.4.34"
368+source = "registry+https://github.com/rust-lang/crates.io-index"
369+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
370+
371+[[package]]
372+name = "memchr"
373+version = "2.8.3"
374+source = "registry+https://github.com/rust-lang/crates.io-index"
375+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
376+
377+[[package]]
378+name = "memmap2"
379+version = "0.9.11"
380+source = "registry+https://github.com/rust-lang/crates.io-index"
381+checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
382+dependencies = [
383+ "libc",
384+]
385+
386+[[package]]
387+name = "minimal-lexical"
388+version = "0.2.1"
389+source = "registry+https://github.com/rust-lang/crates.io-index"
390+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
391+
392+[[package]]
393+name = "miniz_oxide"
394+version = "0.9.1"
395+source = "registry+https://github.com/rust-lang/crates.io-index"
396+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
397+dependencies = [
398+ "adler2",
399+ "simd-adler32",
400+]
401+
402+[[package]]
403+name = "mio"
404+version = "1.2.4"
405+source = "registry+https://github.com/rust-lang/crates.io-index"
406+checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
407+dependencies = [
408+ "libc",
409+ "log",
410+ "wasi",
411+ "windows-sys 0.61.2",
412+]
413+
414+[[package]]
415+name = "nix"
416+version = "0.31.3"
417+source = "registry+https://github.com/rust-lang/crates.io-index"
418+checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
419+dependencies = [
420+ "bitflags",
421+ "cfg-if",
422+ "cfg_aliases",
423+ "libc",
424+]
425+
426+[[package]]
427+name = "nom"
428+version = "7.1.3"
429+source = "registry+https://github.com/rust-lang/crates.io-index"
430+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
431+dependencies = [
432+ "memchr",
433+ "minimal-lexical",
434+]
435+
436+[[package]]
437+name = "num-traits"
438+version = "0.2.19"
439+source = "registry+https://github.com/rust-lang/crates.io-index"
440+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
441+dependencies = [
442+ "autocfg",
443+]
444+
445+[[package]]
446+name = "objc2"
447+version = "0.6.5"
448+source = "registry+https://github.com/rust-lang/crates.io-index"
449+checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8"
450+dependencies = [
451+ "objc2-encode",
452+]
453+
454+[[package]]
455+name = "objc2-encode"
456+version = "4.1.0"
457+source = "registry+https://github.com/rust-lang/crates.io-index"
458+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
459+
460+[[package]]
461+name = "objc2-foundation"
462+version = "0.3.2"
463+source = "registry+https://github.com/rust-lang/crates.io-index"
464+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
465+dependencies = [
466+ "bitflags",
467+ "objc2",
468+]
469+
470+[[package]]
471+name = "objc2-metal"
472+version = "0.3.2"
473+source = "registry+https://github.com/rust-lang/crates.io-index"
474+checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
475+dependencies = [
476+ "bitflags",
477+ "objc2",
478+ "objc2-foundation",
479+]
480+
481+[[package]]
482+name = "parking_lot"
483+version = "0.12.5"
484+source = "registry+https://github.com/rust-lang/crates.io-index"
485+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
486+dependencies = [
487+ "lock_api",
488+ "parking_lot_core",
489+]
490+
491+[[package]]
492+name = "parking_lot_core"
493+version = "0.9.12"
494+source = "registry+https://github.com/rust-lang/crates.io-index"
495+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
496+dependencies = [
497+ "cfg-if",
498+ "libc",
499+ "redox_syscall",
500+ "smallvec",
501+ "windows-link",
502+]
503+
504+[[package]]
505+name = "pkg-config"
506+version = "0.3.34"
507+source = "registry+https://github.com/rust-lang/crates.io-index"
508+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
509+
510+[[package]]
511+name = "prettyplease"
512+version = "0.2.37"
513+source = "registry+https://github.com/rust-lang/crates.io-index"
514+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
515+dependencies = [
516+ "proc-macro2",
517+ "syn 2.0.119",
518+]
519+
520+[[package]]
521+name = "proc-macro2"
522+version = "1.0.107"
523+source = "registry+https://github.com/rust-lang/crates.io-index"
524+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
525+dependencies = [
526+ "unicode-ident",
527+]
528+
529+[[package]]
530+name = "quote"
531+version = "1.0.47"
532+source = "registry+https://github.com/rust-lang/crates.io-index"
533+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
534+dependencies = [
535+ "proc-macro2",
536+]
537+
538+[[package]]
539+name = "redox_syscall"
540+version = "0.5.18"
541+source = "registry+https://github.com/rust-lang/crates.io-index"
542+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
543+dependencies = [
544+ "bitflags",
545+]
546+
547+[[package]]
548+name = "regex"
549+version = "1.13.1"
550+source = "registry+https://github.com/rust-lang/crates.io-index"
551+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
552+dependencies = [
553+ "aho-corasick",
554+ "memchr",
555+ "regex-automata",
556+ "regex-syntax",
557+]
558+
559+[[package]]
560+name = "regex-automata"
561+version = "0.4.18"
562+source = "registry+https://github.com/rust-lang/crates.io-index"
563+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
564+dependencies = [
565+ "aho-corasick",
566+ "memchr",
567+ "regex-syntax",
568+]
569+
570+[[package]]
571+name = "regex-syntax"
572+version = "0.8.11"
573+source = "registry+https://github.com/rust-lang/crates.io-index"
574+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
575+
576+[[package]]
577+name = "roxmltree"
578+version = "0.20.0"
579+source = "registry+https://github.com/rust-lang/crates.io-index"
580+checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
581+
582+[[package]]
583+name = "rquickjs"
584+version = "0.9.0"
585+source = "registry+https://github.com/rust-lang/crates.io-index"
586+checksum = "5c5227859c4dfc83f428e58f9569bf439e628c8d139020e7faff437e6f5abaa0"
587+dependencies = [
588+ "rquickjs-core",
589+]
590+
591+[[package]]
592+name = "rquickjs-core"
593+version = "0.9.0"
594+source = "registry+https://github.com/rust-lang/crates.io-index"
595+checksum = "e82e0ca83028ad5b533b53b96c395bbaab905a5774de4aaf1004eeacafa3d85d"
596+dependencies = [
597+ "rquickjs-sys",
598+]
599+
600+[[package]]
601+name = "rquickjs-sys"
602+version = "0.9.0"
603+source = "registry+https://github.com/rust-lang/crates.io-index"
604+checksum = "7fed0097b0b4fbb2a87f6dd3b995a7c64ca56de30007eb7e867dfdfc78324ba5"
605+dependencies = [
606+ "cc",
607+]
608+
609+[[package]]
610+name = "rustc-hash"
611+version = "2.1.3"
612+source = "registry+https://github.com/rust-lang/crates.io-index"
613+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
614+
615+[[package]]
616+name = "rustix"
617+version = "0.38.44"
618+source = "registry+https://github.com/rust-lang/crates.io-index"
619+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
620+dependencies = [
621+ "bitflags",
622+ "errno",
623+ "libc",
624+ "linux-raw-sys 0.4.15",
625+ "windows-sys 0.59.0",
626+]
627+
628+[[package]]
629+name = "rustix"
630+version = "1.1.5"
631+source = "registry+https://github.com/rust-lang/crates.io-index"
632+checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
633+dependencies = [
634+ "bitflags",
635+ "errno",
636+ "libc",
637+ "linux-raw-sys 0.12.1",
638+ "windows-sys 0.61.2",
639+]
640+
641+[[package]]
642+name = "rustybuzz"
643+version = "0.20.1"
644+source = "registry+https://github.com/rust-lang/crates.io-index"
645+checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"
646+dependencies = [
647+ "bitflags",
648+ "bytemuck",
649+ "core_maths",
650+ "log",
651+ "smallvec",
652+ "ttf-parser",
653+ "unicode-bidi-mirroring",
654+ "unicode-ccc",
655+ "unicode-properties",
656+ "unicode-script",
657+]
658+
659+[[package]]
660+name = "scopeguard"
661+version = "1.2.0"
662+source = "registry+https://github.com/rust-lang/crates.io-index"
663+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
664+
665+[[package]]
666+name = "serde"
667+version = "1.0.229"
668+source = "registry+https://github.com/rust-lang/crates.io-index"
669+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
670+dependencies = [
671+ "serde_core",
672+]
673+
674+[[package]]
675+name = "serde_core"
676+version = "1.0.229"
677+source = "registry+https://github.com/rust-lang/crates.io-index"
678+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
679+dependencies = [
680+ "serde_derive",
681+]
682+
683+[[package]]
684+name = "serde_derive"
685+version = "1.0.229"
686+source = "registry+https://github.com/rust-lang/crates.io-index"
687+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
688+dependencies = [
689+ "proc-macro2",
690+ "quote",
691+ "syn 3.0.6",
692+]
693+
694+[[package]]
695+name = "serde_json"
696+version = "1.0.151"
697+source = "registry+https://github.com/rust-lang/crates.io-index"
698+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
699+dependencies = [
700+ "itoa",
701+ "memchr",
702+ "serde",
703+ "serde_core",
704+ "zmij",
705+]
706+
707+[[package]]
708+name = "serde_spanned"
709+version = "1.1.2"
710+source = "registry+https://github.com/rust-lang/crates.io-index"
711+checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
712+dependencies = [
713+ "serde_core",
714+]
715+
716+[[package]]
717+name = "shlex"
718+version = "1.3.0"
719+source = "registry+https://github.com/rust-lang/crates.io-index"
720+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
721+
722+[[package]]
723+name = "shlex"
724+version = "2.0.1"
725+source = "registry+https://github.com/rust-lang/crates.io-index"
726+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
727+
728+[[package]]
729+name = "signal-hook"
730+version = "0.3.18"
731+source = "registry+https://github.com/rust-lang/crates.io-index"
732+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
733+dependencies = [
734+ "libc",
735+ "signal-hook-registry",
736+]
737+
738+[[package]]
739+name = "signal-hook-mio"
740+version = "0.2.5"
741+source = "registry+https://github.com/rust-lang/crates.io-index"
742+checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
743+dependencies = [
744+ "libc",
745+ "mio",
746+ "signal-hook",
747+]
748+
749+[[package]]
750+name = "signal-hook-registry"
751+version = "1.4.8"
752+source = "registry+https://github.com/rust-lang/crates.io-index"
753+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
754+dependencies = [
755+ "errno",
756+ "libc",
757+]
758+
759+[[package]]
760+name = "simd-adler32"
761+version = "0.3.10"
762+source = "registry+https://github.com/rust-lang/crates.io-index"
763+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
764+
765+[[package]]
766+name = "siphasher"
767+version = "1.0.4"
768+source = "registry+https://github.com/rust-lang/crates.io-index"
769+checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
770+
771+[[package]]
772+name = "skia-bindings"
773+version = "0.153.3"
774+source = "registry+https://github.com/rust-lang/crates.io-index"
775+checksum = "eb325787c91b3f6c34a7c6f2b8a2297b20292d733ca53a08c233e5028aec49af"
776+dependencies = [
777+ "bindgen",
778+ "cc",
779+ "flate2",
780+ "heck",
781+ "pkg-config",
782+ "regex",
783+ "serde_json",
784+ "tar",
785+ "toml",
786+]
787+
788+[[package]]
789+name = "skia-safe"
790+version = "0.153.3"
791+source = "registry+https://github.com/rust-lang/crates.io-index"
792+checksum = "c61cb643dac43067f3eb76c866635a74377ac0abca0ff2590674e1e295514c3d"
793+dependencies = [
794+ "bitflags",
795+ "skia-bindings",
796+]
797+
798+[[package]]
799+name = "slotmap"
800+version = "1.1.1"
801+source = "registry+https://github.com/rust-lang/crates.io-index"
802+checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
803+dependencies = [
804+ "version_check",
805+]
806+
807+[[package]]
808+name = "smallvec"
809+version = "1.16.2"
810+source = "registry+https://github.com/rust-lang/crates.io-index"
811+checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
812+
813+[[package]]
814+name = "svgtypes"
815+version = "0.15.3"
816+source = "registry+https://github.com/rust-lang/crates.io-index"
817+checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
818+dependencies = [
819+ "kurbo",
820+ "siphasher",
821+]
822+
823+[[package]]
824+name = "syn"
825+version = "2.0.119"
826+source = "registry+https://github.com/rust-lang/crates.io-index"
827+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
828+dependencies = [
829+ "proc-macro2",
830+ "quote",
831+ "unicode-ident",
832+]
833+
834+[[package]]
835+name = "syn"
836+version = "3.0.6"
837+source = "registry+https://github.com/rust-lang/crates.io-index"
838+checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
839+dependencies = [
840+ "proc-macro2",
841+ "quote",
842+ "unicode-ident",
843+]
844+
845+[[package]]
846+name = "tar"
847+version = "0.4.46"
848+source = "registry+https://github.com/rust-lang/crates.io-index"
849+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
850+dependencies = [
851+ "filetime",
852+ "libc",
853+ "xattr",
854+]
855+
856+[[package]]
857+name = "tauri-native-host"
858+version = "0.0.1"
859+dependencies = [
860+ "base64",
861+ "crossterm",
862+ "ctrlc",
863+ "fontdb",
864+ "libc",
865+ "objc2",
866+ "objc2-metal",
867+ "rquickjs",
868+ "rustybuzz",
869+ "skia-safe",
870+ "svgtypes",
871+ "unicode-script",
872+ "unicode-segmentation",
873+]
874+
875+[[package]]
876+name = "tinyvec"
877+version = "1.13.3"
878+source = "registry+https://github.com/rust-lang/crates.io-index"
879+checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
880+
881+[[package]]
882+name = "toml"
883+version = "1.1.7+spec-1.1.0"
884+source = "registry+https://github.com/rust-lang/crates.io-index"
885+checksum = "7874cd34dd99040fc24e30923aa64a60a81ff3ddfe2b8129ccc84ba24949deed"
886+dependencies = [
887+ "indexmap",
888+ "serde_core",
889+ "serde_spanned",
890+ "toml_datetime",
891+ "toml_parser",
892+ "toml_writer",
893+ "winnow",
894+]
895+
896+[[package]]
897+name = "toml_datetime"
898+version = "1.1.2+spec-1.1.0"
899+source = "registry+https://github.com/rust-lang/crates.io-index"
900+checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
901+dependencies = [
902+ "serde_core",
903+]
904+
905+[[package]]
906+name = "toml_parser"
907+version = "1.1.4+spec-1.1.0"
908+source = "registry+https://github.com/rust-lang/crates.io-index"
909+checksum = "bed810ee007d8e7f9cf20b4774e7198142fa27186e0f805871da7f3c39eaf333"
910+dependencies = [
911+ "winnow",
912+]
913+
914+[[package]]
915+name = "toml_writer"
916+version = "1.1.3+spec-1.1.0"
917+source = "registry+https://github.com/rust-lang/crates.io-index"
918+checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
919+
920+[[package]]
921+name = "ttf-parser"
922+version = "0.25.1"
923+source = "registry+https://github.com/rust-lang/crates.io-index"
924+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
925+dependencies = [
926+ "core_maths",
927+]
928+
929+[[package]]
930+name = "unicode-bidi-mirroring"
931+version = "0.4.0"
932+source = "registry+https://github.com/rust-lang/crates.io-index"
933+checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe"
934+
935+[[package]]
936+name = "unicode-ccc"
937+version = "0.4.0"
938+source = "registry+https://github.com/rust-lang/crates.io-index"
939+checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
940+
941+[[package]]
942+name = "unicode-ident"
943+version = "1.0.26"
944+source = "registry+https://github.com/rust-lang/crates.io-index"
945+checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
946+
947+[[package]]
948+name = "unicode-properties"
949+version = "0.1.4"
950+source = "registry+https://github.com/rust-lang/crates.io-index"
951+checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
952+
953+[[package]]
954+name = "unicode-script"
955+version = "0.5.8"
956+source = "registry+https://github.com/rust-lang/crates.io-index"
957+checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee"
958+
959+[[package]]
960+name = "unicode-segmentation"
961+version = "1.13.3"
962+source = "registry+https://github.com/rust-lang/crates.io-index"
963+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
964+
965+[[package]]
966+name = "version_check"
967+version = "0.9.5"
968+source = "registry+https://github.com/rust-lang/crates.io-index"
969+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
970+
971+[[package]]
972+name = "wasi"
973+version = "0.11.1+wasi-snapshot-preview1"
974+source = "registry+https://github.com/rust-lang/crates.io-index"
975+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
976+
977+[[package]]
978+name = "winapi"
979+version = "0.3.9"
980+source = "registry+https://github.com/rust-lang/crates.io-index"
981+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
982+dependencies = [
983+ "winapi-i686-pc-windows-gnu",
984+ "winapi-x86_64-pc-windows-gnu",
985+]
986+
987+[[package]]
988+name = "winapi-i686-pc-windows-gnu"
989+version = "0.4.0"
990+source = "registry+https://github.com/rust-lang/crates.io-index"
991+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
992+
993+[[package]]
994+name = "winapi-x86_64-pc-windows-gnu"
995+version = "0.4.0"
996+source = "registry+https://github.com/rust-lang/crates.io-index"
997+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
998+
999+[[package]]
1000+name = "windows-link"
1001+version = "0.2.1"
1002+source = "registry+https://github.com/rust-lang/crates.io-index"
1003+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
1004+
1005+[[package]]
1006+name = "windows-sys"
1007+version = "0.59.0"
1008+source = "registry+https://github.com/rust-lang/crates.io-index"
1009+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
1010+dependencies = [
1011+ "windows-targets",
1012+]
1013+
1014+[[package]]
1015+name = "windows-sys"
1016+version = "0.61.2"
1017+source = "registry+https://github.com/rust-lang/crates.io-index"
1018+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
1019+dependencies = [
1020+ "windows-link",
1021+]
1022+
1023+[[package]]
1024+name = "windows-targets"
1025+version = "0.52.6"
1026+source = "registry+https://github.com/rust-lang/crates.io-index"
1027+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
1028+dependencies = [
1029+ "windows_aarch64_gnullvm",
1030+ "windows_aarch64_msvc",
1031+ "windows_i686_gnu",
1032+ "windows_i686_gnullvm",
1033+ "windows_i686_msvc",
1034+ "windows_x86_64_gnu",
1035+ "windows_x86_64_gnullvm",
1036+ "windows_x86_64_msvc",
1037+]
1038+
1039+[[package]]
1040+name = "windows_aarch64_gnullvm"
1041+version = "0.52.6"
1042+source = "registry+https://github.com/rust-lang/crates.io-index"
1043+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
1044+
1045+[[package]]
1046+name = "windows_aarch64_msvc"
1047+version = "0.52.6"
1048+source = "registry+https://github.com/rust-lang/crates.io-index"
1049+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
1050+
1051+[[package]]
1052+name = "windows_i686_gnu"
1053+version = "0.52.6"
1054+source = "registry+https://github.com/rust-lang/crates.io-index"
1055+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
1056+
1057+[[package]]
1058+name = "windows_i686_gnullvm"
1059+version = "0.52.6"
1060+source = "registry+https://github.com/rust-lang/crates.io-index"
1061+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
1062+
1063+[[package]]
1064+name = "windows_i686_msvc"
1065+version = "0.52.6"
1066+source = "registry+https://github.com/rust-lang/crates.io-index"
1067+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
1068+
1069+[[package]]
1070+name = "windows_x86_64_gnu"
1071+version = "0.52.6"
1072+source = "registry+https://github.com/rust-lang/crates.io-index"
1073+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
1074+
1075+[[package]]
1076+name = "windows_x86_64_gnullvm"
1077+version = "0.52.6"
1078+source = "registry+https://github.com/rust-lang/crates.io-index"
1079+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
1080+
1081+[[package]]
1082+name = "windows_x86_64_msvc"
1083+version = "0.52.6"
1084+source = "registry+https://github.com/rust-lang/crates.io-index"
1085+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1086+
1087+[[package]]
1088+name = "winnow"
1089+version = "1.0.4"
1090+source = "registry+https://github.com/rust-lang/crates.io-index"
1091+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
1092+
1093+[[package]]
1094+name = "xattr"
1095+version = "1.6.1"
1096+source = "registry+https://github.com/rust-lang/crates.io-index"
1097+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
1098+dependencies = [
1099+ "libc",
1100+ "rustix 1.1.5",
1101+]
1102+
1103+[[package]]
1104+name = "zlib-rs"
1105+version = "0.6.8"
1106+source = "registry+https://github.com/rust-lang/crates.io-index"
1107+checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
1108+
1109+[[package]]
1110+name = "zmij"
1111+version = "1.0.23"
1112+source = "registry+https://github.com/rust-lang/crates.io-index"
1113+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
native/runtime/Cargo.toml+3−1
@@ -5,8 +5,10 @@ edition = "2021"
55license = "MIT"
66description = "Embedded PureScript JavaScript host with native graphics and terminal primitives"
77
8−[dependencies]
8+[target.'cfg(not(target_os = "macos"))'.dependencies]
99rquickjs = "0.9.0"
10+
11+[dependencies]
1012# The common build stays display-independent; macOS terminal sessions add Metal
1113# below. SVG/textlayout are unused; text is shaped by Rustybuzz.
1214skia-safe = "=0.153.3"
native/runtime/src/graphics.rs+64−61
@@ -150,8 +150,6 @@ pub(crate) struct Graphics {
150150 surfaces: HashMap<u32, Raster>,
151151 #[cfg(target_os = "macos")]
152152 metal: Option<MetalBackend>,
153− #[cfg(target_os = "macos")]
154− terminal_graphics_active: bool,
155153 next_surface: u32,
156154}
157155
@@ -208,8 +206,6 @@ impl Graphics {
208206 surfaces: HashMap::new(),
209207 #[cfg(target_os = "macos")]
210208 metal: None,
211− #[cfg(target_os = "macos")]
212− terminal_graphics_active: false,
213209 next_surface: 1,
214210 })
215211 }
@@ -456,76 +452,74 @@ impl Graphics {
456452 .ok_or_else(|| failure(format!("unknown canvas surface: {id}")))
457453 }
458454
459− pub fn begin_terminal_graphics(&mut self) -> HostResult<()> {
455+ /// Initialize Metal without changing the backend of any canvas allocation.
456+ pub fn ensure_metal(&mut self) -> HostResult<()> {
460457 #[cfg(target_os = "macos")]
461458 {
462459 if self.metal.is_none() {
463460 self.metal = Some(MetalBackend::new()?);
464461 }
465− self.terminal_graphics_active = true;
466462 Ok(())
467463 }
468464 #[cfg(not(target_os = "macos"))]
469465 {
470− Err(failure(
471− "GPU terminal graphics are unsupported on this platform",
472− ))
473− }
474− }
475−
476− pub fn end_terminal_graphics(&mut self) {
477− // Do not drop Metal here: existing Ganesh surfaces retain GPU resources
478− // and must outlive the DirectContext. This changes only future creates.
479− #[cfg(target_os = "macos")]
480− {
481− self.terminal_graphics_active = false;
466+ Err(failure("Metal canvases are unsupported on this platform"))
482467 }
483468 }
484469
485− pub fn create(&mut self, width: i32, height: i32) -> HostResult<u32> {
470+ pub fn create(&mut self, requested_backend: &str, width: i32, height: i32) -> HostResult<u32> {
486471 rgba_size(width, height)?;
487472 let id = self.next_surface;
488473 let next = id
489474 .checked_add(1)
490475 .ok_or_else(|| failure("canvas handles exhausted"))?;
491− #[cfg(target_os = "macos")]
492− let (mut surface, backend) = if self.terminal_graphics_active {
493− let info = ImageInfo::new(
494− (width, height),
495− ColorType::RGBA8888,
496− AlphaType::Premul,
497− None,
498− );
499− let metal = self
500− .metal
501− .as_mut()
502− .ok_or_else(|| failure("Metal terminal graphics backend is not initialized"))?;
503− let surface = gpu::surfaces::render_target(
504− &mut metal.context,
505− gpu::Budgeted::Yes,
506− &info,
507− 0usize,
508− gpu::SurfaceOrigin::TopLeft,
509− None,
510− false,
511− false,
512− )
513− .ok_or_else(|| failure(format!("could not allocate {width}x{height} Metal canvas")))?;
514− (surface, SurfaceBackend::Metal)
515− } else {
516− (
476+ let (mut surface, backend) = match requested_backend {
477+ "cpu" => (
517478 surfaces::raster_n32_premul((width, height)).ok_or_else(|| {
518479 failure(format!("could not allocate {width}x{height} canvas"))
519480 })?,
520481 SurfaceBackend::Cpu,
521− )
482+ ),
483+ "metal" => {
484+ #[cfg(target_os = "macos")]
485+ {
486+ self.ensure_metal()?;
487+ let info = ImageInfo::new(
488+ (width, height),
489+ ColorType::RGBA8888,
490+ AlphaType::Premul,
491+ None,
492+ );
493+ let metal = self
494+ .metal
495+ .as_mut()
496+ .ok_or_else(|| failure("Metal canvas backend is not initialized"))?;
497+ let surface = gpu::surfaces::render_target(
498+ &mut metal.context,
499+ gpu::Budgeted::Yes,
500+ &info,
501+ 0usize,
502+ gpu::SurfaceOrigin::TopLeft,
503+ None,
504+ false,
505+ false,
506+ )
507+ .ok_or_else(|| {
508+ failure(format!("could not allocate {width}x{height} Metal canvas"))
509+ })?;
510+ (surface, SurfaceBackend::Metal)
511+ }
512+ #[cfg(not(target_os = "macos"))]
513+ {
514+ return Err(failure("Metal canvases are unsupported on this platform"));
515+ }
516+ }
517+ _ => {
518+ return Err(failure(format!(
519+ "unknown canvas backend: {requested_backend:?}"
520+ )))
521+ }
522522 };
523− #[cfg(not(target_os = "macos"))]
524− let (mut surface, backend) = (
525− surfaces::raster_n32_premul((width, height))
526− .ok_or_else(|| failure(format!("could not allocate {width}x{height} canvas")))?,
527− SurfaceBackend::Cpu,
528− );
529523 // Preserve an untouched root clip so reset can discard every user clip.
530524 surface.canvas().save();
531525 self.surfaces.insert(
@@ -953,12 +947,11 @@ mod metal_tests {
953947 use super::*;
954948
955949 #[test]
956− fn terminal_canvases_are_metal_backed_and_read_straight_rgba() {
950+ fn metal_canvases_are_metal_backed_and_read_straight_rgba() {
957951 let mut graphics = Graphics::new(&[]).expect("system fonts are available");
958− graphics
959− .begin_terminal_graphics()
960− .expect("Metal Ganesh initializes");
961− let gpu = graphics.create(1, 1).expect("Metal surface allocates");
952+ let gpu = graphics
953+ .create("metal", 1, 1)
954+ .expect("Metal surface allocates");
962955 assert!(
963956 graphics
964957 .raster(gpu)
@@ -966,7 +959,7 @@ mod metal_tests {
966959 .surface
967960 .image_snapshot()
968961 .is_texture_backed(),
969− "terminal surface must be a Ganesh texture, not a CPU raster"
962+ "Metal selection must allocate a Ganesh texture, not a CPU raster"
970963 );
971964
972965 graphics
@@ -984,8 +977,7 @@ mod metal_tests {
984977 );
985978 }
986979
987− graphics.end_terminal_graphics();
988− let cpu = graphics.create(1, 1).expect("CPU surface allocates");
980+ let cpu = graphics.create("cpu", 1, 1).expect("CPU surface allocates");
989981 assert!(
990982 !graphics
991983 .raster(cpu)
@@ -993,7 +985,18 @@ mod metal_tests {
993985 .surface
994986 .image_snapshot()
995987 .is_texture_backed(),
996− "ending terminal graphics returns subsequent exports to CPU"
988+ "CPU selection must remain independent of existing Metal canvases"
989+ );
990+ graphics
991+ .clear(cpu, [255, 0, 0, 255])
992+ .expect("CPU canvas clears");
993+ let mut after_cpu = [0; 4];
994+ graphics
995+ .read_rgba(gpu, 1, 1, &mut after_cpu)
996+ .expect("Metal canvas remains readable");
997+ assert_eq!(
998+ after_cpu, rgba,
999+ "CPU drawing must not change a Metal canvas"
9971000 );
9981001 }
9991002}
native/runtime/src/jsc.rsnew file+1293
@@ -0,0 +1,1293 @@
1+//! macOS JavaScriptCore implementation of the synchronous native host.
2+//!
3+//! This uses Apple's supported C API directly. Keeping the bridge in C values
4+//! avoids serializing drawing calls and lets path values stay in the JavaScript
5+//! array until Skia consumes each numeric element.
6+
7+use super::{
8+ absolute_path, failure, write_output, Config, Host, HostResult, Terminal, INTERRUPT, RUN_LOCK,
9+};
10+use std::cell::RefCell;
11+use std::collections::HashMap;
12+use std::error::Error;
13+use std::ffi::{c_char, c_void};
14+use std::io::{self, IsTerminal};
15+use std::panic::{catch_unwind, AssertUnwindSafe};
16+use std::ptr;
17+use std::rc::Rc;
18+use std::sync::atomic::{AtomicBool, Ordering};
19+use std::sync::Arc;
20+use std::time::Instant;
21+
22+use super::graphics;
23+
24+#[allow(non_camel_case_types)]
25+type JSContextRef = *const OpaqueJSContext;
26+#[allow(non_camel_case_types)]
27+type JSContextGroupRef = *const OpaqueJSContextGroup;
28+#[allow(non_camel_case_types)]
29+type JSGlobalContextRef = *mut OpaqueJSContext;
30+#[allow(non_camel_case_types)]
31+type JSStringRef = *mut OpaqueJSString;
32+#[allow(non_camel_case_types)]
33+type JSValueRef = *const OpaqueJSValue;
34+#[allow(non_camel_case_types)]
35+type JSObjectRef = *mut OpaqueJSValue;
36+
37+#[repr(C)]
38+struct OpaqueJSContext {
39+ _private: [u8; 0],
40+}
41+#[repr(C)]
42+struct OpaqueJSContextGroup {
43+ _private: [u8; 0],
44+}
45+#[repr(C)]
46+struct OpaqueJSString {
47+ _private: [u8; 0],
48+}
49+#[repr(C)]
50+struct OpaqueJSValue {
51+ _private: [u8; 0],
52+}
53+
54+type JSObjectCallAsFunctionCallback = unsafe extern "C" fn(
55+ JSContextRef,
56+ JSObjectRef,
57+ JSObjectRef,
58+ usize,
59+ *const JSValueRef,
60+ *mut JSValueRef,
61+) -> JSValueRef;
62+type JSShouldTerminateCallback = unsafe extern "C" fn(JSContextRef, *mut c_void) -> bool;
63+
64+#[link(name = "JavaScriptCore", kind = "framework")]
65+unsafe extern "C" {
66+ fn JSContextGroupCreate() -> JSContextGroupRef;
67+ fn JSContextGroupRelease(group: JSContextGroupRef);
68+ // Declared by JavaScriptCore's C API watchdog header on macOS. The SDK's
69+ // umbrella header omits it, but the framework exports it since macOS 10.6.
70+ fn JSContextGroupSetExecutionTimeLimit(
71+ group: JSContextGroupRef,
72+ limit: f64,
73+ callback: Option<JSShouldTerminateCallback>,
74+ context: *mut c_void,
75+ );
76+ fn JSContextGroupClearExecutionTimeLimit(group: JSContextGroupRef);
77+ fn JSGlobalContextCreateInGroup(
78+ group: JSContextGroupRef,
79+ class: *const c_void,
80+ ) -> JSGlobalContextRef;
81+ fn JSGlobalContextRelease(context: JSGlobalContextRef);
82+ fn JSContextGetGroup(context: JSContextRef) -> JSContextGroupRef;
83+ fn JSContextGetGlobalObject(context: JSContextRef) -> JSObjectRef;
84+ fn JSEvaluateScript(
85+ context: JSContextRef,
86+ script: JSStringRef,
87+ this_object: JSObjectRef,
88+ source_url: JSStringRef,
89+ starting_line: i32,
90+ exception: *mut JSValueRef,
91+ ) -> JSValueRef;
92+ fn JSObjectMake(context: JSContextRef, class: *const c_void, data: *mut c_void) -> JSObjectRef;
93+ fn JSObjectMakeArray(
94+ context: JSContextRef,
95+ count: usize,
96+ values: *const JSValueRef,
97+ exception: *mut JSValueRef,
98+ ) -> JSObjectRef;
99+ fn JSObjectIsFunction(context: JSContextRef, object: JSObjectRef) -> bool;
100+ fn JSObjectSetPropertyAtIndex(
101+ context: JSContextRef,
102+ object: JSObjectRef,
103+ index: u32,
104+ value: JSValueRef,
105+ exception: *mut JSValueRef,
106+ );
107+ fn JSObjectMakeFunctionWithCallback(
108+ context: JSContextRef,
109+ name: JSStringRef,
110+ callback: JSObjectCallAsFunctionCallback,
111+ ) -> JSObjectRef;
112+ fn JSObjectGetProperty(
113+ context: JSContextRef,
114+ object: JSObjectRef,
115+ name: JSStringRef,
116+ exception: *mut JSValueRef,
117+ ) -> JSValueRef;
118+ fn JSObjectSetProperty(
119+ context: JSContextRef,
120+ object: JSObjectRef,
121+ name: JSStringRef,
122+ value: JSValueRef,
123+ attributes: u32,
124+ exception: *mut JSValueRef,
125+ );
126+ fn JSObjectGetPropertyAtIndex(
127+ context: JSContextRef,
128+ object: JSObjectRef,
129+ index: u32,
130+ exception: *mut JSValueRef,
131+ ) -> JSValueRef;
132+ fn JSObjectCallAsFunction(
133+ context: JSContextRef,
134+ object: JSObjectRef,
135+ this_object: JSObjectRef,
136+ argument_count: usize,
137+ arguments: *const JSValueRef,
138+ exception: *mut JSValueRef,
139+ ) -> JSValueRef;
140+ fn JSValueGetType(context: JSContextRef, value: JSValueRef) -> i32;
141+ fn JSValueIsUndefined(context: JSContextRef, value: JSValueRef) -> bool;
142+ fn JSValueIsNull(context: JSContextRef, value: JSValueRef) -> bool;
143+ fn JSValueIsArray(context: JSContextRef, value: JSValueRef) -> bool;
144+ fn JSValueMakeUndefined(context: JSContextRef) -> JSValueRef;
145+ fn JSValueMakeBoolean(context: JSContextRef, value: bool) -> JSValueRef;
146+ fn JSValueMakeNumber(context: JSContextRef, value: f64) -> JSValueRef;
147+ fn JSValueMakeString(context: JSContextRef, value: JSStringRef) -> JSValueRef;
148+ fn JSValueProtect(context: JSContextRef, value: JSValueRef);
149+ fn JSValueUnprotect(context: JSContextRef, value: JSValueRef);
150+ fn JSValueToBoolean(context: JSContextRef, value: JSValueRef) -> bool;
151+ fn JSValueToNumber(context: JSContextRef, value: JSValueRef, exception: *mut JSValueRef)
152+ -> f64;
153+ fn JSValueToStringCopy(
154+ context: JSContextRef,
155+ value: JSValueRef,
156+ exception: *mut JSValueRef,
157+ ) -> JSStringRef;
158+ fn JSStringCreateWithCharacters(characters: *const u16, length: usize) -> JSStringRef;
159+ fn JSStringRelease(value: JSStringRef);
160+ fn JSStringGetMaximumUTF8CStringSize(value: JSStringRef) -> usize;
161+ fn JSStringGetUTF8CString(value: JSStringRef, buffer: *mut c_char, size: usize) -> usize;
162+}
163+
164+const JS_TYPE_BOOLEAN: i32 = 2;
165+const JS_TYPE_NUMBER: i32 = 3;
166+const JS_TYPE_STRING: i32 = 4;
167+const JS_TYPE_OBJECT: i32 = 5;
168+const WATCHDOG_PERIOD: f64 = 0.01;
169+
170+struct JsString(JSStringRef);
171+
172+impl JsString {
173+ fn new(value: &str) -> Result<Self, JsFailure> {
174+ let characters: Vec<u16> = value.encode_utf16().collect();
175+ let string = unsafe { JSStringCreateWithCharacters(characters.as_ptr(), characters.len()) };
176+ if string.is_null() {
177+ Err(JsFailure::error("could not allocate JavaScript string"))
178+ } else {
179+ Ok(Self(string))
180+ }
181+ }
182+
183+ fn copy_to_string(value: JSStringRef) -> String {
184+ unsafe {
185+ let size = JSStringGetMaximumUTF8CStringSize(value);
186+ let mut bytes = Vec::<u8>::with_capacity(size);
187+ let written = JSStringGetUTF8CString(value, bytes.as_mut_ptr().cast(), size);
188+ if written == 0 {
189+ return String::new();
190+ }
191+ bytes.set_len(written - 1);
192+ match String::from_utf8(bytes) {
193+ Ok(value) => value,
194+ Err(error) => String::from_utf8_lossy(error.as_bytes()).into_owned(),
195+ }
196+ }
197+ }
198+}
199+
200+impl Drop for JsString {
201+ fn drop(&mut self) {
202+ unsafe { JSStringRelease(self.0) }
203+ }
204+}
205+
206+struct ProtectedValue {
207+ context: JSContextRef,
208+ value: JSValueRef,
209+}
210+
211+impl ProtectedValue {
212+ fn new(context: JSContextRef, value: JSValueRef) -> Self {
213+ unsafe { JSValueProtect(context, value) };
214+ Self { context, value }
215+ }
216+}
217+
218+impl Drop for ProtectedValue {
219+ fn drop(&mut self) {
220+ unsafe { JSValueUnprotect(self.context, self.value) };
221+ }
222+}
223+
224+enum JsFailure {
225+ Thrown(JSValueRef),
226+ Error(&'static str),
227+ Message(String),
228+ Type(&'static str),
229+ TypeMessage(String),
230+ Range(&'static str),
231+}
232+
233+impl JsFailure {
234+ fn error(message: &'static str) -> Self {
235+ Self::Error(message)
236+ }
237+
238+ fn native(error: Box<dyn Error>) -> Self {
239+ Self::Message(error.to_string())
240+ }
241+}
242+
243+struct Api {
244+ context: JSContextRef,
245+}
246+
247+impl Api {
248+ fn native<T>(&self, result: HostResult<T>) -> Result<T, JsFailure> {
249+ result.map_err(JsFailure::native)
250+ }
251+
252+ fn check_exception(&self, value: JSValueRef) -> Result<(), JsFailure> {
253+ if value.is_null() {
254+ Ok(())
255+ } else {
256+ Err(JsFailure::Thrown(value))
257+ }
258+ }
259+
260+ fn value_type(&self, value: JSValueRef) -> i32 {
261+ unsafe { JSValueGetType(self.context, value) }
262+ }
263+
264+ fn argument<'a>(&self, args: &'a [JSValueRef], index: usize) -> Result<JSValueRef, JsFailure> {
265+ args.get(index)
266+ .copied()
267+ .ok_or(JsFailure::Type("missing native argument"))
268+ }
269+
270+ fn string_value(&self, value: JSValueRef) -> Result<String, JsFailure> {
271+ if self.value_type(value) != JS_TYPE_STRING {
272+ return Err(JsFailure::Type("native argument must be a string"));
273+ }
274+ unsafe {
275+ let mut exception = ptr::null();
276+ let string_ref = JSValueToStringCopy(self.context, value, &mut exception);
277+ self.check_exception(exception)?;
278+ if string_ref.is_null() {
279+ return Err(JsFailure::error("could not read JavaScript string"));
280+ }
281+ let string = JsString::copy_to_string(string_ref);
282+ JSStringRelease(string_ref);
283+ Ok(string)
284+ }
285+ }
286+
287+ fn arg_string(&self, args: &[JSValueRef], index: usize) -> Result<String, JsFailure> {
288+ self.string_value(self.argument(args, index)?)
289+ }
290+
291+ fn optional_string(
292+ &self,
293+ args: &[JSValueRef],
294+ index: usize,
295+ ) -> Result<Option<String>, JsFailure> {
296+ let value = self.argument(args, index)?;
297+ if unsafe { JSValueIsUndefined(self.context, value) || JSValueIsNull(self.context, value) }
298+ {
299+ Ok(None)
300+ } else {
301+ self.string_value(value).map(Some)
302+ }
303+ }
304+
305+ fn boolean_value(&self, value: JSValueRef) -> Result<bool, JsFailure> {
306+ if self.value_type(value) != JS_TYPE_BOOLEAN {
307+ return Err(JsFailure::Type("native argument must be a boolean"));
308+ }
309+ Ok(unsafe { JSValueToBoolean(self.context, value) })
310+ }
311+
312+ fn boolean(&self, args: &[JSValueRef], index: usize) -> Result<bool, JsFailure> {
313+ self.boolean_value(self.argument(args, index)?)
314+ }
315+
316+ fn number_value(&self, value: JSValueRef) -> Result<f64, JsFailure> {
317+ if self.value_type(value) != JS_TYPE_NUMBER {
318+ return Err(JsFailure::Type("native argument must be a number"));
319+ }
320+ unsafe {
321+ let mut exception = ptr::null();
322+ let number = JSValueToNumber(self.context, value, &mut exception);
323+ self.check_exception(exception)?;
324+ Ok(number)
325+ }
326+ }
327+
328+ fn f32(&self, args: &[JSValueRef], index: usize) -> Result<f32, JsFailure> {
329+ Ok(self.number_value(self.argument(args, index)?)? as f32)
330+ }
331+
332+ fn i32(&self, args: &[JSValueRef], index: usize) -> Result<i32, JsFailure> {
333+ let number = self.number_value(self.argument(args, index)?)?;
334+ if number < i32::MIN as f64 || number > i32::MAX as f64 {
335+ return Err(JsFailure::Range("native integer argument is out of range"));
336+ }
337+ Ok(number as i32)
338+ }
339+
340+ fn u32(&self, args: &[JSValueRef], index: usize) -> Result<u32, JsFailure> {
341+ let number = self.number_value(self.argument(args, index)?)?;
342+ if number < 0.0 || number > u32::MAX as f64 {
343+ return Err(JsFailure::Range(
344+ "native unsigned integer argument is out of range",
345+ ));
346+ }
347+ Ok(number as u32)
348+ }
349+
350+ fn object_value(&self, value: JSValueRef) -> Result<JSObjectRef, JsFailure> {
351+ if self.value_type(value) != JS_TYPE_OBJECT {
352+ return Err(JsFailure::Type("native argument must be an object"));
353+ }
354+ Ok(value.cast_mut())
355+ }
356+
357+ fn object(&self, args: &[JSValueRef], index: usize) -> Result<JSObjectRef, JsFailure> {
358+ self.object_value(self.argument(args, index)?)
359+ }
360+
361+ fn optional_object(
362+ &self,
363+ args: &[JSValueRef],
364+ index: usize,
365+ ) -> Result<Option<JSObjectRef>, JsFailure> {
366+ let value = self.argument(args, index)?;
367+ if unsafe { JSValueIsUndefined(self.context, value) || JSValueIsNull(self.context, value) }
368+ {
369+ Ok(None)
370+ } else {
371+ self.object_value(value).map(Some)
372+ }
373+ }
374+
375+ fn property(&self, object: JSObjectRef, name: &str) -> Result<JSValueRef, JsFailure> {
376+ let name = JsString::new(name)?;
377+ unsafe {
378+ let mut exception = ptr::null();
379+ let value = JSObjectGetProperty(self.context, object, name.0, &mut exception);
380+ self.check_exception(exception)?;
381+ Ok(value)
382+ }
383+ }
384+
385+ fn property_string(&self, object: JSObjectRef, name: &str) -> Result<String, JsFailure> {
386+ self.string_value(self.property(object, name)?)
387+ }
388+
389+ fn property_f32(&self, object: JSObjectRef, name: &str) -> Result<f32, JsFailure> {
390+ Ok(self.number_value(self.property(object, name)?)? as f32)
391+ }
392+
393+ fn property_i32(&self, object: JSObjectRef, name: &str) -> Result<i32, JsFailure> {
394+ let value = self.number_value(self.property(object, name)?)?;
395+ if value < i32::MIN as f64 || value > i32::MAX as f64 {
396+ return Err(JsFailure::Range("native integer property is out of range"));
397+ }
398+ Ok(value as i32)
399+ }
400+
401+ fn property_u32(&self, object: JSObjectRef, name: &str) -> Result<u32, JsFailure> {
402+ let value = self.number_value(self.property(object, name)?)?;
403+ if value < 0.0 || value > u32::MAX as f64 {
404+ return Err(JsFailure::Range(
405+ "native unsigned integer property is out of range",
406+ ));
407+ }
408+ Ok(value as u32)
409+ }
410+
411+ fn strings(&self, args: &[JSValueRef], index: usize) -> Result<Vec<String>, JsFailure> {
412+ let value = self.argument(args, index)?;
413+ if !unsafe { JSValueIsArray(self.context, value) } {
414+ return Err(JsFailure::Type("native argument must be an array"));
415+ }
416+ let array = value.cast_mut();
417+ let length = self.property_u32(array, "length")?;
418+ let mut values = Vec::with_capacity(length as usize);
419+ for index in 0..length {
420+ unsafe {
421+ let mut exception = ptr::null();
422+ let value = JSObjectGetPropertyAtIndex(self.context, array, index, &mut exception);
423+ self.check_exception(exception)?;
424+ values.push(self.string_value(value)?);
425+ }
426+ }
427+ Ok(values)
428+ }
429+
430+ fn color(&self, object: JSObjectRef) -> Result<graphics::Color, JsFailure> {
431+ Ok([
432+ self.property_u32(object, "r")? as u8,
433+ self.property_u32(object, "g")? as u8,
434+ self.property_u32(object, "b")? as u8,
435+ self.property_u32(object, "a")? as u8,
436+ ])
437+ }
438+
439+ fn affine(&self, object: JSObjectRef) -> Result<[f32; 6], JsFailure> {
440+ Ok([
441+ self.property_f32(object, "a")?,
442+ self.property_f32(object, "b")?,
443+ self.property_f32(object, "c")?,
444+ self.property_f32(object, "d")?,
445+ self.property_f32(object, "e")?,
446+ self.property_f32(object, "f")?,
447+ ])
448+ }
449+
450+ fn stroke(&self, object: JSObjectRef) -> Result<graphics::Stroke, JsFailure> {
451+ Ok(graphics::Stroke {
452+ color: self.color(self.object_value(self.property(object, "color")?)?)?,
453+ width: self.property_f32(object, "width")?,
454+ join: self.property_i32(object, "join")?,
455+ cap: self.property_i32(object, "cap")?,
456+ })
457+ }
458+
459+ fn path(&self, object: JSObjectRef) -> Result<skia_safe::Path, JsFailure> {
460+ let values_value = self.property(object, "values")?;
461+ if !unsafe { JSValueIsArray(self.context, values_value) } {
462+ return Err(JsFailure::Type("canvas path values must be an array"));
463+ }
464+ let values = self.object_value(values_value)?;
465+ let offset = self.property_i32(object, "offset")?;
466+ let length = self.property_i32(object, "length")?;
467+ let array_length = self.property_u32(values, "length")?;
468+ let end = offset
469+ .checked_add(length)
470+ .filter(|end| offset >= 0 && length >= 0 && (*end as u32) <= array_length)
471+ .ok_or(JsFailure::Range("invalid canvas path range"))?;
472+ self.native(graphics::path_from_values((offset as u32..end as u32).map(
473+ |index| unsafe {
474+ let mut exception = ptr::null();
475+ let value = JSObjectGetPropertyAtIndex(self.context, values, index, &mut exception);
476+ self.check_exception(exception)
477+ .and_then(|()| self.number_value(value))
478+ .map_err(|error| failure(self.failure_message(error)))
479+ },
480+ )))
481+ }
482+
483+ fn failure_message(&self, error: JsFailure) -> String {
484+ match error {
485+ JsFailure::Thrown(value) => self.exception_string(value),
486+ JsFailure::Error(message) | JsFailure::Type(message) | JsFailure::Range(message) => {
487+ message.into()
488+ }
489+ JsFailure::Message(message) | JsFailure::TypeMessage(message) => message,
490+ }
491+ }
492+
493+ fn undefined(&self) -> JSValueRef {
494+ unsafe { JSValueMakeUndefined(self.context) }
495+ }
496+ fn bool(&self, value: bool) -> JSValueRef {
497+ unsafe { JSValueMakeBoolean(self.context, value) }
498+ }
499+ fn number(&self, value: impl Into<f64>) -> JSValueRef {
500+ unsafe { JSValueMakeNumber(self.context, value.into()) }
501+ }
502+
503+ fn string(&self, value: &str) -> Result<JSValueRef, JsFailure> {
504+ let value = JsString::new(value)?;
505+ Ok(unsafe { JSValueMakeString(self.context, value.0) })
506+ }
507+
508+ fn argument_array(&self, arguments: &[String]) -> Result<JSValueRef, JsFailure> {
509+ let mut exception = ptr::null();
510+ let array = unsafe { JSObjectMakeArray(self.context, 0, ptr::null(), &mut exception) };
511+ self.check_exception(exception)?;
512+ if array.is_null() {
513+ return Err(JsFailure::error(
514+ "could not allocate JavaScript argument array",
515+ ));
516+ }
517+ // Insert each value immediately into a rooted array rather than retain
518+ // unrooted JS strings in a Rust heap vector while subsequent strings allocate.
519+ let array_root = ProtectedValue::new(self.context, array.cast_const());
520+ for (index, value) in arguments.iter().enumerate() {
521+ let value = self.string(value)?;
522+ unsafe {
523+ JSObjectSetPropertyAtIndex(
524+ self.context,
525+ array,
526+ index as u32,
527+ value,
528+ &mut exception,
529+ );
530+ }
531+ self.check_exception(exception)?;
532+ }
533+ Ok(array_root.value)
534+ }
535+
536+ fn object_output(&self) -> JSObjectRef {
537+ unsafe { JSObjectMake(self.context, ptr::null(), ptr::null_mut()) }
538+ }
539+
540+ fn set(&self, object: JSObjectRef, name: &str, value: JSValueRef) -> Result<(), JsFailure> {
541+ let name = JsString::new(name)?;
542+ unsafe {
543+ let mut exception = ptr::null();
544+ JSObjectSetProperty(self.context, object, name.0, value, 0, &mut exception);
545+ self.check_exception(exception)
546+ }
547+ }
548+
549+ fn exception_string(&self, value: JSValueRef) -> String {
550+ let description = unsafe {
551+ let mut exception = ptr::null();
552+ let string = JSValueToStringCopy(self.context, value, &mut exception);
553+ if self.check_exception(exception).is_err() || string.is_null() {
554+ return "JavaScript exception (message could not be read)".into();
555+ }
556+ let result = JsString::copy_to_string(string);
557+ JSStringRelease(string);
558+ result
559+ };
560+ if self.value_type(value) == JS_TYPE_OBJECT {
561+ if let Ok(stack) = self.property(value.cast_mut(), "stack") {
562+ if let Ok(stack) = self.string_value(stack) {
563+ if !stack.is_empty() && !stack.starts_with(&description) {
564+ return format!("{description}\n{stack}");
565+ }
566+ }
567+ }
568+ }
569+ description
570+ }
571+
572+ fn make_exception(&self, error: JsFailure) -> JSValueRef {
573+ if let JsFailure::Thrown(value) = error {
574+ return value;
575+ }
576+ let (name, message) = match error {
577+ JsFailure::Error(message) => ("Error", message.into()),
578+ JsFailure::Message(message) => ("Error", message),
579+ JsFailure::Type(message) => ("TypeError", message.into()),
580+ JsFailure::TypeMessage(message) => ("TypeError", message),
581+ JsFailure::Range(message) => ("RangeError", message.into()),
582+ JsFailure::Thrown(_) => unreachable!(),
583+ };
584+ let script = format!("new {name}({})", js_string_literal(&message));
585+ let script = match JsString::new(&script) {
586+ Ok(value) => value,
587+ Err(_) => return self.undefined(),
588+ };
589+ unsafe {
590+ let mut exception = ptr::null();
591+ let value = JSEvaluateScript(
592+ self.context,
593+ script.0,
594+ ptr::null_mut(),
595+ ptr::null_mut(),
596+ 1,
597+ &mut exception,
598+ );
599+ if exception.is_null() {
600+ value
601+ } else {
602+ exception
603+ }
604+ }
605+ }
606+}
607+
608+fn js_string_literal(value: &str) -> String {
609+ let mut quoted = String::with_capacity(value.len() + 2);
610+ quoted.push('"');
611+ for character in value.chars() {
612+ match character {
613+ '\\' => quoted.push_str("\\\\"),
614+ '"' => quoted.push_str("\\\""),
615+ '\n' => quoted.push_str("\\n"),
616+ '\r' => quoted.push_str("\\r"),
617+ '\u{2028}' => quoted.push_str("\\u2028"),
618+ '\u{2029}' => quoted.push_str("\\u2029"),
619+ character if character.is_control() => {
620+ use std::fmt::Write;
621+ let _ = write!(quoted, "\\u{:04x}", character as u32);
622+ }
623+ character => quoted.push(character),
624+ }
625+ }
626+ quoted.push('"');
627+ quoted
628+}
629+
630+fn dispatch(
631+ api: &Api,
632+ host: &mut Host,
633+ name: &str,
634+ args: &[JSValueRef],
635+) -> Result<JSValueRef, JsFailure> {
636+ macro_rules! result {
637+ ($value:expr) => {
638+ api.native($value).map(|_| api.undefined())
639+ };
640+ }
641+ match name {
642+ "arguments" => api.argument_array(&host.config.arguments),
643+ "version" => api.string(&host.config.version),
644+ "isStdinTTY" => Ok(api.bool(io::stdin().is_terminal())),
645+ "readStdin" => api
646+ .native(host.read_stdin())
647+ .and_then(|value| api.string(&value)),
648+ "readLine" => match api.native(host.read_line())? {
649+ Some(value) => api.string(&value),
650+ None => Ok(api.undefined()),
651+ },
652+ "readText" => api
653+ .native(std::fs::read_to_string(api.arg_string(args, 0)?).map_err(Into::into))
654+ .and_then(|value| api.string(&value)),
655+ "writeText" => result!(
656+ std::fs::write(api.arg_string(args, 0)?, api.arg_string(args, 1)?).map_err(Into::into)
657+ ),
658+ "stdout" => result!(write_output(
659+ &mut io::stdout().lock(),
660+ &api.arg_string(args, 0)?
661+ )),
662+ "stderr" => result!(write_output(
663+ &mut io::stderr().lock(),
664+ &api.arg_string(args, 0)?
665+ )),
666+ "absolutePath" => api
667+ .native(absolute_path(&api.arg_string(args, 0)?))
668+ .and_then(|value| api.string(&value)),
669+ "environment" => api.string(&std::env::var(api.arg_string(args, 0)?).unwrap_or_default()),
670+ "monotonicMilliseconds" => Ok(api.number(host.started.elapsed().as_secs_f64() * 1000.0)),
671+ "platform" => api.string(std::env::consts::OS),
672+ "runApplicationTask" | "runApplication" => {
673+ let outcome = if name == "runApplicationTask" {
674+ api.native(
675+ host.application_task(api.strings(args, 0)?, api.optional_string(args, 1)?),
676+ )?
677+ } else {
678+ api.native(host.application(api.strings(args, 0)?, api.optional_string(args, 1)?))?
679+ };
680+ let value = api.object_output();
681+ api.set(value, "success", api.bool(outcome.success))?;
682+ api.set(value, "code", api.number(outcome.code))?;
683+ api.set(value, "stderr", api.string(&outcome.stderr)?)?;
684+ Ok(value.cast_const())
685+ }
686+ "measureText" => {
687+ let font = api.arg_string(args, 0)?;
688+ let text = api.arg_string(args, 1)?;
689+ let measured = api.native(
690+ host.graphics()
691+ .and_then(|graphics| graphics.measure(&font, &text)),
692+ )?;
693+ let value = api.object_output();
694+ api.set(value, "width", api.number(measured.width))?;
695+ api.set(value, "ascent", api.number(measured.ascent))?;
696+ api.set(value, "descent", api.number(measured.descent))?;
697+ Ok(value.cast_const())
698+ }
699+ "canvasCreate" => {
700+ let backend = api.arg_string(args, 0)?;
701+ let options = api.object(args, 1)?;
702+ let graphics = api.native(host.graphics())?;
703+ api.native(graphics.create(
704+ &backend,
705+ api.property_i32(options, "width")?,
706+ api.property_i32(options, "height")?,
707+ ))
708+ .map(|value| api.number(value))
709+ }
710+ "canvasRelease" => result!(api.native(host.graphics())?.release(api.u32(args, 0)?)),
711+ "canvasReset" => result!(api.native(host.graphics())?.reset(api.u32(args, 0)?)),
712+ "canvasClear" => {
713+ let id = api.u32(args, 0)?;
714+ let color = api.color(api.object(args, 1)?)?;
715+ result!(api.native(host.graphics())?.clear(id, color))
716+ }
717+ "canvasSave" => result!(api.native(host.graphics())?.save(api.u32(args, 0)?)),
718+ "canvasRestore" => result!(api.native(host.graphics())?.restore(api.u32(args, 0)?)),
719+ "canvasSetTransform" => {
720+ let id = api.u32(args, 0)?;
721+ let matrix = api.affine(api.object(args, 1)?)?;
722+ result!(api.native(host.graphics())?.set_transform(id, matrix))
723+ }
724+ "canvasTransform" => {
725+ let id = api.u32(args, 0)?;
726+ let matrix = api.affine(api.object(args, 1)?)?;
727+ result!(api.native(host.graphics())?.transform(id, matrix))
728+ }
729+ "canvasClip" => {
730+ let id = api.u32(args, 0)?;
731+ let path = api.path(api.object(args, 1)?)?;
732+ let even_odd = api.boolean(args, 2)?;
733+ result!(api.native(host.graphics())?.clip(id, &path, even_odd))
734+ }
735+ "canvasSetBlend" => {
736+ let id = api.u32(args, 0)?;
737+ let mode = api.arg_string(args, 1)?;
738+ result!(api.native(host.graphics())?.set_blend(id, &mode))
739+ }
740+ "canvasSetBlur" => {
741+ let id = api.u32(args, 0)?;
742+ let radius = api.f32(args, 1)?;
743+ result!(api.native(host.graphics())?.set_blur(id, radius))
744+ }
745+ "canvasSetTint" => {
746+ let id = api.u32(args, 0)?;
747+ let color = api
748+ .optional_object(args, 1)?
749+ .map(|value| api.color(value))
750+ .transpose()?;
751+ result!(api.native(host.graphics())?.set_tint(id, color))
752+ }
753+ "canvasFillPath" => {
754+ let id = api.u32(args, 0)?;
755+ let path = api.path(api.object(args, 1)?)?;
756+ let color = api.color(api.object(args, 2)?)?;
757+ result!(api.native(host.graphics())?.fill_path(id, &path, color))
758+ }
759+ "canvasStrokePath" => {
760+ let id = api.u32(args, 0)?;
761+ let path = api.path(api.object(args, 1)?)?;
762+ let stroke = api.stroke(api.object(args, 2)?)?;
763+ result!(api.native(host.graphics())?.stroke_path(id, &path, stroke))
764+ }
765+ "canvasFillStrokePath" => {
766+ let id = api.u32(args, 0)?;
767+ let path = api.path(api.object(args, 1)?)?;
768+ let color = api.color(api.object(args, 2)?)?;
769+ let stroke = api.stroke(api.object(args, 3)?)?;
770+ result!(api
771+ .native(host.graphics())?
772+ .fill_stroke_path(id, &path, color, stroke))
773+ }
774+ "canvasText" => {
775+ let id = api.u32(args, 0)?;
776+ let options = api.object(args, 1)?;
777+ let font = api.property_string(options, "font")?;
778+ let text = api.property_string(options, "content")?;
779+ let x = api.property_f32(options, "x")?;
780+ let y = api.property_f32(options, "y")?;
781+ let color = api.color(api.object_value(api.property(options, "color")?)?)?;
782+ let align = api.property_i32(options, "align")?;
783+ let baseline = api.property_i32(options, "baseline")?;
784+ result!(api
785+ .native(host.graphics())?
786+ .text(id, &font, &text, x, y, color, align, baseline))
787+ }
788+ "canvasCirclePattern" => {
789+ let id = api.u32(args, 0)?;
790+ let options = api.object(args, 1)?;
791+ let rect = api.object_value(api.property(options, "rect")?)?;
792+ let pattern = graphics::CirclePattern {
793+ rect: [
794+ api.property_f32(rect, "x")?,
795+ api.property_f32(rect, "y")?,
796+ api.property_f32(rect, "width")?,
797+ api.property_f32(rect, "height")?,
798+ ],
799+ background: api.color(api.object_value(api.property(options, "background")?)?)?,
800+ ink: api.color(api.object_value(api.property(options, "ink")?)?)?,
801+ tile: api.property_f32(options, "tile")?,
802+ radius: api.property_f32(options, "radius")?,
803+ origin: [
804+ api.property_f32(options, "originX")?,
805+ api.property_f32(options, "originY")?,
806+ ],
807+ };
808+ result!(api.native(host.graphics())?.circle_pattern(id, pattern))
809+ }
810+ "canvasSurface" => result!(api
811+ .native(host.graphics())?
812+ .surface(api.u32(args, 0)?, api.u32(args, 1)?)),
813+ "canvasWritePng" => {
814+ let id = api.u32(args, 0)?;
815+ let path = api.arg_string(args, 1)?;
816+ result!(api.native(host.graphics())?.png(id, &path))
817+ }
818+ "sharedFrameOpen" => result!(host.open_shared_frames()),
819+ "sharedFrameCapture" => api
820+ .native(host.capture_shared_frame(api.u32(args, 0)?))
821+ .map(|value| api.number(value)),
822+ "sharedFrameInfo" => {
823+ let id = api.u32(args, 0)?;
824+ let (name, width, height, byte_length) = api.native(host.shared_frame_info(id))?;
825+ let value = api.object_output();
826+ api.set(value, "id", api.number(id))?;
827+ api.set(
828+ value,
829+ "name",
830+ api.string(std::str::from_utf8(name).expect("shared-memory names are ASCII"))?,
831+ )?;
832+ api.set(value, "width", api.number(width))?;
833+ api.set(value, "height", api.number(height))?;
834+ api.set(value, "byteLength", api.number(byte_length))?;
835+ Ok(value.cast_const())
836+ }
837+ "sharedFrameConsumed" => api
838+ .native(host.shared_frame_consumed(api.u32(args, 0)?))
839+ .map(|value| api.bool(value)),
840+
841+ "sharedFrameRelease" => result!(host.release_shared_frame(api.u32(args, 0)?)),
842+ "sharedFrameClose" => result!(host.close_shared_frames()),
843+ "beginVideo" => {
844+ let options = api.object(args, 0)?;
845+ api.native(host.begin_video(
846+ api.property_string(options, "output")?,
847+ api.property_i32(options, "fps")?,
848+ api.property_i32(options, "width")?,
849+ api.property_i32(options, "height")?,
850+ ))
851+ .map(|value| api.number(value))
852+ }
853+ "writeVideoFrame" => result!(host.video_frame(api.u32(args, 0)?, api.u32(args, 1)?)),
854+ "finishVideo" => result!(host.finish_video(api.u32(args, 0)?)),
855+ "cancelVideo" => result!(host.cancel_video(api.u32(args, 0)?)),
856+ "enterTerminal" => result!(host.terminal.enter()),
857+ "restoreTerminal" => result!(host.terminal.restore()),
858+ "terminalSize" => {
859+ let (width, height) = if io::stdin().is_terminal() || io::stdout().is_terminal() {
860+ api.native(crossterm::terminal::size().map_err(Into::into))?
861+ } else {
862+ (0, 0)
863+ };
864+ let value = api.object_output();
865+ api.set(value, "width", api.number(width))?;
866+ api.set(value, "height", api.number(height))?;
867+ Ok(value.cast_const())
868+ }
869+ "terminalGraphicsInspectSize" => {
870+ let (columns, rows, width, height) = api.native(Terminal::inspect_size())?;
871+ let value = api.object_output();
872+ api.set(value, "columns", api.number(columns))?;
873+ api.set(value, "rows", api.number(rows))?;
874+ api.set(value, "width", api.number(width))?;
875+ api.set(value, "height", api.number(height))?;
876+ Ok(value.cast_const())
877+ }
878+ "terminalGraphicsOpen" => result!(host.open_terminal_graphics()),
879+ "terminalGraphicsSize" => {
880+ let (columns, rows, width, height) = api.native(host.terminal.graphics_size())?;
881+ let value = api.object_output();
882+ api.set(value, "columns", api.number(columns))?;
883+ api.set(value, "rows", api.number(rows))?;
884+ api.set(value, "width", api.number(width))?;
885+ api.set(value, "height", api.number(height))?;
886+ Ok(value.cast_const())
887+ }
888+ "terminalGraphicsPresent" => {
889+ result!(host.terminal_present(api.u32(args, 0)?, api.i32(args, 1)?, api.i32(args, 2)?))
890+ }
891+ "terminalGraphicsReadKey" | "readKey" => api
892+ .native(host.terminal.key())
893+ .and_then(|value| api.string(&value)),
894+ "terminalGraphicsClose" => result!(host.close_terminal_graphics()),
895+ "sleepMilliseconds" => result!(host.sleep(api.i32(args, 0)?)),
896+ _ => Err(JsFailure::TypeMessage(format!(
897+ "unknown native primitive: {name}"
898+ ))),
899+ }
900+}
901+
902+struct JscHost {
903+ state: Rc<RefCell<Host>>,
904+}
905+thread_local! { static HOSTS: RefCell<HashMap<usize, Rc<JscHost>>> = RefCell::new(HashMap::new()); }
906+
907+unsafe extern "C" fn native_call(
908+ context: JSContextRef,
909+ _function: JSObjectRef,
910+ _this: JSObjectRef,
911+ count: usize,
912+ values: *const JSValueRef,
913+ exception: *mut JSValueRef,
914+) -> JSValueRef {
915+ let api = Api { context };
916+ let result = catch_unwind(AssertUnwindSafe(|| {
917+ let host = HOSTS
918+ .with(|hosts| hosts.borrow().get(&(context as usize)).cloned())
919+ .ok_or(JsFailure::error("native host is unavailable"))?;
920+ if count == 0 {
921+ return Err(JsFailure::Type("missing native primitive name"));
922+ }
923+ let values = unsafe { std::slice::from_raw_parts(values, count) };
924+ let name = api.string_value(values[0])?;
925+ let mut host = host
926+ .state
927+ .try_borrow_mut()
928+ .map_err(|_| JsFailure::error("native host cannot be called recursively"))?;
929+ dispatch(&api, &mut host, &name, &values[1..])
930+ }));
931+ match result {
932+ Ok(Ok(value)) => value,
933+ Ok(Err(error)) => {
934+ unsafe {
935+ *exception = api.make_exception(error);
936+ }
937+ api.undefined()
938+ }
939+ Err(_) => {
940+ unsafe {
941+ *exception = api.make_exception(JsFailure::error("native host panicked"));
942+ }
943+ api.undefined()
944+ }
945+ }
946+}
947+
948+unsafe extern "C" fn should_terminate(context: JSContextRef, data: *mut c_void) -> bool {
949+ let interrupted = unsafe { (&*(data as *const AtomicBool)).load(Ordering::Relaxed) };
950+ if !interrupted {
951+ // Keep polling after a non-terminating callback, including on WebKit
952+ // versions that do not automatically restart the elapsed timer.
953+ unsafe {
954+ JSContextGroupSetExecutionTimeLimit(
955+ JSContextGetGroup(context),
956+ WATCHDOG_PERIOD,
957+ Some(should_terminate),
958+ data,
959+ );
960+ }
961+ }
962+ interrupted
963+}
964+
965+struct Context {
966+ group: JSContextGroupRef,
967+ context: JSGlobalContextRef,
968+ state: Rc<RefCell<Host>>,
969+}
970+
971+impl Context {
972+ fn new(state: Rc<RefCell<Host>>) -> HostResult<Self> {
973+ let group = unsafe { JSContextGroupCreate() };
974+ if group.is_null() {
975+ return Err(failure("could not create JavaScriptCore context group"));
976+ }
977+ let context = unsafe { JSGlobalContextCreateInGroup(group, ptr::null()) };
978+ if context.is_null() {
979+ unsafe { JSContextGroupRelease(group) };
980+ return Err(failure("could not create JavaScriptCore context"));
981+ }
982+ unsafe {
983+ JSContextGroupSetExecutionTimeLimit(
984+ group,
985+ WATCHDOG_PERIOD,
986+ Some(should_terminate),
987+ Arc::as_ptr(&state.borrow().interrupted).cast_mut().cast(),
988+ );
989+ }
990+ HOSTS.with(|hosts| {
991+ hosts.borrow_mut().insert(
992+ context as usize,
993+ Rc::new(JscHost {
994+ state: state.clone(),
995+ }),
996+ );
997+ });
998+ Ok(Self {
999+ group,
1000+ context,
1001+ state,
1002+ })
1003+ }
1004+
1005+ fn close(&mut self) -> HostResult<()> {
1006+ if self.context.is_null() {
1007+ return Ok(());
1008+ }
1009+ HOSTS.with(|hosts| {
1010+ hosts.borrow_mut().remove(&(self.context as usize));
1011+ });
1012+ unsafe { JSContextGroupClearExecutionTimeLimit(self.group) };
1013+ let cleanup = match self.state.try_borrow_mut() {
1014+ Ok(mut state) => state.shutdown(),
1015+ Err(_) => Err(failure("native host is still borrowed during shutdown")),
1016+ };
1017+ unsafe {
1018+ JSGlobalContextRelease(self.context);
1019+ JSContextGroupRelease(self.group);
1020+ }
1021+ self.context = ptr::null_mut();
1022+ self.group = ptr::null();
1023+ cleanup
1024+ }
1025+}
1026+
1027+impl Drop for Context {
1028+ fn drop(&mut self) {
1029+ let _ = self.close();
1030+ }
1031+}
1032+
1033+fn execute(api: &Api, script: &str) -> Result<i32, JsFailure> {
1034+ let global = unsafe { JSContextGetGlobalObject(api.context) };
1035+ let call_name = JsString::new("call")?;
1036+ let host = api.object_output();
1037+ let call = unsafe { JSObjectMakeFunctionWithCallback(api.context, call_name.0, native_call) };
1038+ api.set(host, "call", call.cast_const())?;
1039+ api.set(global, "__purescriptNativeHost", host.cast_const())?;
1040+ let source = JsString::new(script)?;
1041+ let mut exception = ptr::null();
1042+ unsafe {
1043+ JSEvaluateScript(
1044+ api.context,
1045+ source.0,
1046+ ptr::null_mut(),
1047+ ptr::null_mut(),
1048+ 1,
1049+ &mut exception,
1050+ );
1051+ }
1052+ api.check_exception(exception)?;
1053+ let main = api.property(global, "__purescriptMain")?;
1054+ let main = api.object_value(main)?;
1055+ if !unsafe { JSObjectIsFunction(api.context, main) } {
1056+ return Err(JsFailure::Type("__purescriptMain must be a function"));
1057+ }
1058+ let status = unsafe {
1059+ JSObjectCallAsFunction(
1060+ api.context,
1061+ main,
1062+ ptr::null_mut(),
1063+ 0,
1064+ ptr::null(),
1065+ &mut exception,
1066+ )
1067+ };
1068+ api.check_exception(exception)?;
1069+ let status = api.number_value(status)?;
1070+ if !status.is_finite()
1071+ || status.fract() != 0.0
1072+ || status < i32::MIN as f64
1073+ || status > i32::MAX as f64
1074+ {
1075+ return Err(JsFailure::Type(
1076+ "__purescriptMain must return an integer exit status",
1077+ ));
1078+ }
1079+ Ok(status as i32)
1080+}
1081+
1082+pub(super) fn run(script: &str, config: Config) -> Result<i32, Box<dyn Error>> {
1083+ let _guard = RUN_LOCK
1084+ .lock()
1085+ .map_err(|_| failure("native runtime lock is poisoned"))?;
1086+ let interrupted = INTERRUPT
1087+ .as_ref()
1088+ .map_err(|error| failure(error.clone()))?
1089+ .clone();
1090+ interrupted.store(false, Ordering::Relaxed);
1091+ let state = Rc::new(RefCell::new(Host {
1092+ config,
1093+ graphics: None,
1094+ videos: HashMap::new(),
1095+ next_video: 1,
1096+ frames: HashMap::new(),
1097+ next_frame: 1,
1098+ shared_frames_active: false,
1099+ terminal: Default::default(),
1100+ stdin: io::BufReader::new(io::stdin()),
1101+ started: Instant::now(),
1102+ interrupted: interrupted.clone(),
1103+ }));
1104+ let mut context = Context::new(state)?;
1105+ let api = Api {
1106+ context: context.context.cast_const(),
1107+ };
1108+ let outcome = execute(&api, script).map_err(|error| failure(api.failure_message(error)));
1109+ let cleanup = context.close();
1110+ cleanup?;
1111+ if interrupted.load(Ordering::Relaxed) {
1112+ Ok(130)
1113+ } else {
1114+ outcome
1115+ }
1116+}
1117+
1118+#[cfg(test)]
1119+mod tests {
1120+ use super::run;
1121+ use crate::Config;
1122+ use std::path::PathBuf;
1123+ use std::sync::atomic::{AtomicU64, Ordering};
1124+
1125+ fn config(arguments: Vec<String>) -> Config {
1126+ Config {
1127+ arguments,
1128+ version: "test".into(),
1129+ fonts: Vec::new(),
1130+ application: PathBuf::new(),
1131+ ffmpeg: PathBuf::new(),
1132+ }
1133+ }
1134+
1135+ #[test]
1136+ fn unicode_and_nul_survive_script_arguments_and_native_file_io() {
1137+ static NEXT: AtomicU64 = AtomicU64::new(0);
1138+ let path = std::env::temp_dir().join(format!(
1139+ "tauri-jsc-🦀-{}-{}.txt",
1140+ std::process::id(),
1141+ NEXT.fetch_add(1, Ordering::Relaxed)
1142+ ));
1143+ let result = run(
1144+ r#"
1145+ globalThis.__purescriptMain = () => {
1146+ const host = globalThis.__purescriptNativeHost;
1147+ const path = host.call("arguments")[0];
1148+ const text = "before\u0000🦀 after";
1149+ host.call("writeText", path, text);
1150+ return host.call("readText", path) === text ? 19 : 1;
1151+ };
1152+ "#,
1153+ config(vec![path.to_string_lossy().into_owned()]),
1154+ );
1155+ let contents = std::fs::read_to_string(&path);
1156+ let _ = std::fs::remove_file(&path);
1157+ assert_eq!(result.unwrap(), 19);
1158+ assert_eq!(contents.unwrap(), "before\0🦀 after");
1159+ }
1160+
1161+ #[test]
1162+ fn cancellation_remains_armed_after_earlier_watchdog_periods() {
1163+ let path =
1164+ std::env::temp_dir().join(format!("tauri-jsc-cancel-{}.txt", std::process::id()));
1165+ let ready = path.clone();
1166+ let stopper = std::thread::spawn(move || {
1167+ let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
1168+ while !ready.exists() {
1169+ assert!(std::time::Instant::now() < deadline, "script never started");
1170+ std::thread::sleep(std::time::Duration::from_millis(1));
1171+ }
1172+ std::thread::sleep(std::time::Duration::from_millis(100));
1173+ let signalled = std::time::Instant::now();
1174+ crate::INTERRUPT
1175+ .as_ref()
1176+ .unwrap()
1177+ .store(true, Ordering::Relaxed);
1178+ signalled
1179+ });
1180+ let result = run(
1181+ r#"
1182+ globalThis.__purescriptMain = () => {
1183+ const host = globalThis.__purescriptNativeHost;
1184+ host.call("writeText", host.call("arguments")[0], "ready");
1185+ const end = Date.now() + 2000;
1186+ while (Date.now() < end) {}
1187+ return 0;
1188+ };
1189+ "#,
1190+ config(vec![path.to_string_lossy().into_owned()]),
1191+ );
1192+ let latency = stopper.join().unwrap().elapsed();
1193+ let _ = std::fs::remove_file(path);
1194+ assert_eq!(result.unwrap(), 130);
1195+ assert!(
1196+ latency < std::time::Duration::from_millis(500),
1197+ "cancellation took {latency:?}"
1198+ );
1199+ }
1200+
1201+ #[test]
1202+ fn argument_arrays_keep_all_values_alive_during_allocation() {
1203+ let arguments = (0..20_000)
1204+ .map(|index| format!("{index}:{}\0🦀", "label".repeat(32)))
1205+ .collect();
1206+ let status = run(
1207+ r#"
1208+ globalThis.__purescriptMain = () => {
1209+ const args = globalThis.__purescriptNativeHost.call("arguments");
1210+ if (args.length !== 20000) return 1;
1211+ for (let i = 0; i < args.length; ++i) {
1212+ if (args[i] !== i + ":" + "label".repeat(32) + "\u0000🦀") return 2;
1213+ }
1214+ return 23;
1215+ };
1216+ "#,
1217+ config(arguments),
1218+ )
1219+ .unwrap();
1220+ assert_eq!(status, 23);
1221+ }
1222+
1223+ #[test]
1224+ fn invalid_entry_points_and_exit_statuses_do_not_poison_later_runs() {
1225+ for body in [
1226+ "42",
1227+ "{}",
1228+ "() => NaN",
1229+ "() => Infinity",
1230+ "() => 0.5",
1231+ "() => 2147483648",
1232+ "() => -2147483649",
1233+ ] {
1234+ let source = format!("globalThis.__purescriptMain = {body};");
1235+ assert!(run(&source, config(Vec::new())).is_err(), "{body}");
1236+ }
1237+ assert_eq!(
1238+ run("globalThis.__purescriptMain = () => 37", config(Vec::new())).unwrap(),
1239+ 37
1240+ );
1241+ assert_eq!(
1242+ run(
1243+ "globalThis.__purescriptMain = () => -2147483648",
1244+ config(Vec::new())
1245+ )
1246+ .unwrap(),
1247+ i32::MIN
1248+ );
1249+ assert_eq!(
1250+ run(
1251+ "globalThis.__purescriptMain = () => 2147483647",
1252+ config(Vec::new())
1253+ )
1254+ .unwrap(),
1255+ i32::MAX
1256+ );
1257+ }
1258+
1259+ #[test]
1260+ fn native_errors_keep_types_messages_and_a_usable_canvas() {
1261+ let status = run(
1262+ r#"
1263+ globalThis.__purescriptMain = () => {
1264+ const host = globalThis.__purescriptNativeHost;
1265+ try { host.call("missingPrimitive"); return 1; }
1266+ catch (error) { if (!(error instanceof TypeError)) return 2; }
1267+ const canvas = host.call("canvasCreate", "cpu", {width:4,height:4});
1268+ try {
1269+ host.call("canvasFillPath", canvas,
1270+ {values:[0,0,0],offset:-1,length:3}, {r:0,g:0,b:0,a:255});
1271+ return 3;
1272+ } catch (error) { if (!(error instanceof RangeError)) return 4; }
1273+ try { host.call("canvasClear", canvas, {r:"invalid",g:0,b:0,a:255}); return 5; }
1274+ catch (error) { if (!(error instanceof TypeError)) return 6; }
1275+ host.call("canvasClear", canvas, {r:12,g:34,b:56,a:255});
1276+ host.call("canvasRelease", canvas);
1277+ return 41;
1278+ };
1279+ "#,
1280+ config(Vec::new()),
1281+ )
1282+ .unwrap();
1283+ assert_eq!(status, 41);
1284+ let error = run(
1285+ r#"
1286+ globalThis.__purescriptMain = () => { throw new Error("renderer cause 🦀"); };
1287+ "#,
1288+ config(Vec::new()),
1289+ )
1290+ .unwrap_err();
1291+ assert!(error.to_string().contains("renderer cause 🦀"), "{error}");
1292+ }
1293+}
native/runtime/src/lib.rs+232−23
@@ -1,29 +1,42 @@
11//! Synchronous native primitives for a bundled PureScript JavaScript program.
22//! The host has no graph, document, command-line or application lifecycle policy.
3+//!
4+//! macOS uses system JavaScriptCore; sign the application with
5+//! `com.apple.security.cs.allow-jit` to enable native-code compilation.
6+//! Other platforms use QuickJS.
37
48mod graphics;
9+#[cfg(target_os = "macos")]
10+mod jsc;
511mod process;
612mod terminal;
713mod terminal_shm;
814
15+#[cfg(not(target_os = "macos"))]
16+use rquickjs::function::{Func, Rest};
17+#[cfg(not(target_os = "macos"))]
18+use rquickjs::{
19+ Array, CaughtError, Context, Ctx, Exception, FromJs, Function, IntoJs, Object, Runtime, Value,
20+};
21+#[cfg(not(target_os = "macos"))]
922use std::cell::RefCell;
1023use std::collections::HashMap;
1124use std::error::Error;
12−use std::io::{self, IsTerminal, Read, Write};
25+#[cfg(not(target_os = "macos"))]
26+use std::io::IsTerminal;
27+use std::io::{self, BufRead, Read, Write};
1328use std::path::{Path, PathBuf};
29+#[cfg(not(target_os = "macos"))]
1430use std::rc::Rc;
1531use std::sync::atomic::{AtomicBool, Ordering};
1632use std::sync::{Arc, LazyLock, Mutex};
1733use std::time::{Duration, Instant};
1834
19−use rquickjs::function::{Func, Rest};
20−use rquickjs::{
21− Array, CaughtError, Context, Ctx, Exception, FromJs, Function, IntoJs, Object, Runtime, Value,
22−};
23−
2435use graphics::Graphics;
2536use process::Process;
2637use terminal::Terminal;
38+#[cfg(unix)]
39+use terminal_shm::SharedFrame;
2740
2841pub struct FontAsset {
2942 pub family: &'static str,
@@ -56,12 +69,26 @@ struct Video {
5669 height: i32,
5770}
5871
72+#[cfg(unix)]
73+struct ExportedFrame {
74+ frame: SharedFrame,
75+ width: i32,
76+ height: i32,
77+ byte_length: i32,
78+}
79+
5980struct Host {
6081 config: Config,
6182 graphics: Option<Graphics>,
6283 videos: HashMap<u32, Video>,
6384 next_video: u32,
85+ #[cfg(unix)]
86+ frames: HashMap<u32, ExportedFrame>,
87+ #[cfg(unix)]
88+ next_frame: u32,
89+ shared_frames_active: bool,
6490 terminal: Terminal,
91+ stdin: io::BufReader<io::Stdin>,
6592 started: Instant,
6693 interrupted: Arc<AtomicBool>,
6794}
@@ -74,21 +101,155 @@ impl Host {
74101 Ok(self.graphics.as_mut().expect("graphics initialized above"))
75102 }
76103
104+ fn open_shared_frames(&mut self) -> HostResult<()> {
105+ if self.shared_frames_active {
106+ return Ok(());
107+ }
108+ self.graphics()?.ensure_metal()?;
109+ self.shared_frames_active = true;
110+ Ok(())
111+ }
112+
113+ #[cfg(unix)]
114+ fn capture_shared_frame(&mut self, surface: u32) -> HostResult<u32> {
115+ if !self.shared_frames_active {
116+ return Err(failure("shared-frame renderer has not been opened"));
117+ }
118+ let (width, height, byte_length, frame) = {
119+ let graphics = self.graphics()?;
120+ let (width, height) = graphics.dimensions(surface)?;
121+ if width <= 0 || height <= 0 {
122+ return Err(failure("canvas dimensions must be positive"));
123+ }
124+ let byte_length = (width as usize)
125+ .checked_mul(height as usize)
126+ .and_then(|size| size.checked_mul(4))
127+ .ok_or_else(|| failure("canvas dimensions overflow the RGBA buffer size"))?;
128+ let byte_length = i32::try_from(byte_length)
129+ .map_err(|_| failure("canvas RGBA buffer exceeds native frame limits"))?;
130+ let mut frame = SharedFrame::create(byte_length as usize).map_err(|error| {
131+ failure(format!("could not create shared-memory frame: {error}"))
132+ })?;
133+ graphics.read_rgba(surface, width, height, frame.bytes_mut())?;
134+ (width, height, byte_length, frame)
135+ };
136+ let id = self.next_frame;
137+ let next = id
138+ .checked_add(1)
139+ .filter(|next| *next <= i32::MAX as u32)
140+ .ok_or_else(|| failure("shared-frame handle space exhausted"))?;
141+ self.frames.insert(
142+ id,
143+ ExportedFrame {
144+ frame,
145+ width,
146+ height,
147+ byte_length,
148+ },
149+ );
150+ self.next_frame = next;
151+ Ok(id)
152+ }
153+
154+ #[cfg(not(unix))]
155+ fn capture_shared_frame(&mut self, _surface: u32) -> HostResult<u32> {
156+ Err(failure("shared-memory frames are supported only on Unix"))
157+ }
158+
159+ #[cfg(unix)]
160+ fn shared_frame_info(&self, id: u32) -> HostResult<(&[u8], i32, i32, i32)> {
161+ let frame = self
162+ .frames
163+ .get(&id)
164+ .ok_or_else(|| failure("shared-memory frame is not open"))?;
165+ Ok((
166+ frame.frame.name(),
167+ frame.width,
168+ frame.height,
169+ frame.byte_length,
170+ ))
171+ }
172+
173+ #[cfg(not(unix))]
174+ fn shared_frame_info(&self, _id: u32) -> HostResult<(&[u8], i32, i32, i32)> {
175+ Err(failure("shared-memory frames are supported only on Unix"))
176+ }
177+
178+ #[cfg(unix)]
179+ fn shared_frame_consumed(&self, id: u32) -> HostResult<bool> {
180+ let frame = self
181+ .frames
182+ .get(&id)
183+ .ok_or_else(|| failure("shared-memory frame is not open"))?;
184+ Ok(frame.frame.consumed()?)
185+ }
186+
187+ #[cfg(not(unix))]
188+ fn shared_frame_consumed(&self, _id: u32) -> HostResult<bool> {
189+ Err(failure("shared-memory frames are supported only on Unix"))
190+ }
191+
192+ fn release_shared_frame(&mut self, id: u32) -> HostResult<()> {
193+ #[cfg(unix)]
194+ self.frames.remove(&id);
195+ Ok(())
196+ }
197+
198+ fn close_shared_frames(&mut self) -> HostResult<()> {
199+ #[cfg(unix)]
200+ self.frames.clear();
201+ self.shared_frames_active = false;
202+ Ok(())
203+ }
204+
205+ fn read_stdin(&mut self) -> HostResult<String> {
206+ if self.interrupted.load(Ordering::Relaxed) {
207+ return Err(failure("interrupted"));
208+ }
209+ let mut source = String::new();
210+ self.stdin.read_to_string(&mut source)?;
211+ if self.interrupted.load(Ordering::Relaxed) {
212+ return Err(failure("interrupted"));
213+ }
214+ Ok(source)
215+ }
216+
217+ fn read_line(&mut self) -> HostResult<Option<String>> {
218+ if self.interrupted.load(Ordering::Relaxed) {
219+ return Err(failure("interrupted"));
220+ }
221+ let mut line = String::new();
222+ let bytes = self.stdin.read_line(&mut line)?;
223+ if self.interrupted.load(Ordering::Relaxed) {
224+ return Err(failure("interrupted"));
225+ }
226+ if bytes == 0 {
227+ return Ok(None);
228+ }
229+ if line.ends_with('\n') {
230+ line.pop();
231+ if line.ends_with('\r') {
232+ line.pop();
233+ }
234+ }
235+ Ok(Some(line))
236+ }
237+
77238 fn open_terminal_graphics(&mut self) -> HostResult<()> {
239+ if self.shared_frames_active {
240+ return Err(failure(
241+ "cannot open terminal graphics while shared-frame renderer is open",
242+ ));
243+ }
78244 self.terminal.open_graphics()?;
79− if let Err(error) = self.graphics().and_then(Graphics::begin_terminal_graphics) {
245+ if let Err(error) = self.graphics().and_then(Graphics::ensure_metal) {
80246 let _ = self.terminal.close_graphics();
81247 return Err(error);
82248 }
83249 Ok(())
84250 }
85−
86251 fn close_terminal_graphics(&mut self) -> HostResult<()> {
87− let result = self.terminal.close_graphics();
88− if let Some(graphics) = &mut self.graphics {
89− graphics.end_terminal_graphics();
90− }
91− result
252+ self.terminal.close_graphics()
92253 }
93254
94255 fn terminal_present(&mut self, surface: u32, column: i32, row: i32) -> HostResult<()> {
@@ -243,6 +404,7 @@ impl Host {
243404 }
244405
245406 fn shutdown(&mut self) -> HostResult<()> {
407+ self.close_shared_frames()?;
246408 let mut result = Ok(());
247409 for (_, video) in self.videos.drain() {
248410 let cancelled = video.process.cancel();
@@ -251,17 +413,16 @@ impl Host {
251413 }
252414 }
253415 let restored = self.terminal.restore();
254− if let Some(graphics) = &mut self.graphics {
255− graphics.end_terminal_graphics();
256− }
257416 result.and(restored)
258417 }
259418}
260419
420+#[cfg(not(target_os = "macos"))]
261421fn native<'js, T>(ctx: &Ctx<'js>, result: HostResult<T>) -> rquickjs::Result<T> {
262422 result.map_err(|error| Exception::throw_message(ctx, &error.to_string()))
263423}
264424
425+#[cfg(not(target_os = "macos"))]
265426fn argument<'js, T: FromJs<'js>>(
266427 ctx: &Ctx<'js>,
267428 args: &[Value<'js>],
@@ -273,6 +434,7 @@ fn argument<'js, T: FromJs<'js>>(
273434 T::from_js(ctx, value.clone())
274435}
275436
437+#[cfg(not(target_os = "macos"))]
276438fn canvas_color(value: Object<'_>) -> rquickjs::Result<graphics::Color> {
277439 Ok([
278440 value.get("r")?,
@@ -282,6 +444,7 @@ fn canvas_color(value: Object<'_>) -> rquickjs::Result<graphics::Color> {
282444 ])
283445}
284446
447+#[cfg(not(target_os = "macos"))]
285448fn canvas_affine(value: Object<'_>) -> rquickjs::Result<[f32; 6]> {
286449 Ok([
287450 value.get("a")?,
@@ -293,6 +456,7 @@ fn canvas_affine(value: Object<'_>) -> rquickjs::Result<[f32; 6]> {
293456 ])
294457}
295458
459+#[cfg(not(target_os = "macos"))]
296460fn canvas_stroke(value: Object<'_>) -> rquickjs::Result<graphics::Stroke> {
297461 Ok(graphics::Stroke {
298462 color: canvas_color(value.get("color")?)?,
@@ -302,6 +466,7 @@ fn canvas_stroke(value: Object<'_>) -> rquickjs::Result<graphics::Stroke> {
302466 })
303467}
304468
469+#[cfg(not(target_os = "macos"))]
305470fn canvas_path<'js>(ctx: &Ctx<'js>, value: Object<'js>) -> rquickjs::Result<skia_safe::Path> {
306471 let values: Array<'js> = value.get("values")?;
307472 let offset: i32 = value.get("offset")?;
@@ -320,6 +485,7 @@ fn canvas_path<'js>(ctx: &Ctx<'js>, value: Object<'js>) -> rquickjs::Result<skia
320485 )
321486}
322487
488+#[cfg(not(target_os = "macos"))]
323489fn dispatch<'js>(
324490 ctx: &Ctx<'js>,
325491 host: &mut Host,
@@ -340,11 +506,8 @@ fn dispatch<'js>(
340506 "arguments" => host.config.arguments.as_slice().into_js(ctx),
341507 "version" => host.config.version.as_str().into_js(ctx),
342508 "isStdinTTY" => io::stdin().is_terminal().into_js(ctx),
343− "readStdin" => result!((|| -> HostResult<String> {
344− let mut source = String::new();
345− io::stdin().read_to_string(&mut source)?;
346− Ok(source)
347− })()),
509+ "readStdin" => result!(host.read_stdin()),
510+ "readLine" => result!(host.read_line()),
348511 "readText" => result!(std::fs::read_to_string(arg!(0, String)).map_err(Into::into)),
349512 "writeText" => {
350513 result!(std::fs::write(arg!(0, String), arg!(1, String)).map_err(Into::into))
@@ -398,10 +561,11 @@ fn dispatch<'js>(
398561 value.into_js(ctx)
399562 }
400563 "canvasCreate" => {
401− let options = arg!(0, Object<'js>);
564+ let backend = arg!(0, String);
565+ let options = arg!(1, Object<'js>);
402566 let width = options.get("width")?;
403567 let height = options.get("height")?;
404− result!(native(ctx, host.graphics())?.create(width, height))
568+ result!(native(ctx, host.graphics())?.create(&backend, width, height))
405569 }
406570 "canvasRelease" => {
407571 result!(native(ctx, host.graphics())?.release(arg!(0, u32)))
@@ -513,6 +677,29 @@ fn dispatch<'js>(
513677 let path = arg!(1, String);
514678 result!(native(ctx, host.graphics())?.png(id, &path))
515679 }
680+ "sharedFrameOpen" => result!(host.open_shared_frames()),
681+ "sharedFrameCapture" => result!(host.capture_shared_frame(arg!(0, u32))),
682+ "sharedFrameInfo" => {
683+ let id = arg!(0, u32);
684+ let (name, width, height, byte_length) = native(ctx, host.shared_frame_info(id))?;
685+ let value = Object::new(ctx.clone())?;
686+ value.set(
687+ "id",
688+ i32::try_from(id).expect("shared-frame IDs fit PureScript Int"),
689+ )?;
690+ value.set(
691+ "name",
692+ std::str::from_utf8(name).expect("shared-memory names are ASCII"),
693+ )?;
694+ value.set("width", width)?;
695+ value.set("height", height)?;
696+ value.set("byteLength", byte_length)?;
697+ value.into_js(ctx)
698+ }
699+ "sharedFrameConsumed" => result!(host.shared_frame_consumed(arg!(0, u32))),
700+
701+ "sharedFrameRelease" => result!(host.release_shared_frame(arg!(0, u32))),
702+ "sharedFrameClose" => result!(host.close_shared_frames()),
516703 "beginVideo" => {
517704 let options = arg!(0, Object<'js>);
518705 result!(host.begin_video(
@@ -538,6 +725,15 @@ fn dispatch<'js>(
538725 value.set("height", height)?;
539726 value.into_js(ctx)
540727 }
728+ "terminalGraphicsInspectSize" => {
729+ let (columns, rows, width, height) = native(ctx, Terminal::inspect_size())?;
730+ let value = Object::new(ctx.clone())?;
731+ value.set("columns", columns)?;
732+ value.set("rows", rows)?;
733+ value.set("width", width)?;
734+ value.set("height", height)?;
735+ value.into_js(ctx)
736+ }
541737 "terminalGraphicsOpen" => result!(host.open_terminal_graphics()),
542738 "terminalGraphicsSize" => {
543739 let (columns, rows, width, height) = native(ctx, host.terminal.graphics_size())?;
@@ -592,6 +788,7 @@ static INTERRUPT: LazyLock<Result<Arc<AtomicBool>, String>> = LazyLock::new(|| {
592788 Ok(flag)
593789});
594790
791+#[cfg(not(target_os = "macos"))]
595792fn register<'js>(ctx: &Ctx<'js>, state: Rc<RefCell<Host>>) -> rquickjs::Result<()> {
596793 let host = Object::new(ctx.clone())?;
597794 host.set(
@@ -615,6 +812,7 @@ fn register<'js>(ctx: &Ctx<'js>, state: Rc<RefCell<Host>>) -> rquickjs::Result<(
615812/// integer exit status. Native exceptions become ordinary JS Error instances.
616813/// Terminal state and child processes are released on success, JS errors and
617814/// Rust unwinding. Only one terminal-owning runtime may run in a process at once.
815+#[cfg(not(target_os = "macos"))]
618816pub fn run(script: &str, config: Config) -> Result<i32, Box<dyn Error>> {
619817 let _guard = RUN_LOCK
620818 .lock()
@@ -629,7 +827,13 @@ pub fn run(script: &str, config: Config) -> Result<i32, Box<dyn Error>> {
629827 graphics: None,
630828 videos: HashMap::new(),
631829 next_video: 1,
830+ #[cfg(unix)]
831+ frames: HashMap::new(),
832+ #[cfg(unix)]
833+ next_frame: 1,
834+ shared_frames_active: false,
632835 terminal: Terminal::default(),
836+ stdin: io::BufReader::new(io::stdin()),
633837 started: Instant::now(),
634838 interrupted: interrupted.clone(),
635839 }));
@@ -674,3 +878,8 @@ pub fn run(script: &str, config: Config) -> Result<i32, Box<dyn Error>> {
674878 outcome
675879 }
676880}
881+
882+#[cfg(target_os = "macos")]
883+pub fn run(script: &str, config: Config) -> Result<i32, Box<dyn Error>> {
884+ jsc::run(script, config)
885+}
native/runtime/src/terminal.rs+45
@@ -259,6 +259,16 @@ impl Terminal {
259259 }
260260 }
261261
262+ /// Inspect the controlling terminal's cell and pixel dimensions without
263+ /// reading terminal input, changing terminal state, or writing escapes.
264+ /// Missing controlling terminals report zeros; ioctl failures propagate.
265+ pub fn inspect_size() -> HostResult<(u16, u16, u16, u16)> {
266+ #[cfg(not(unix))]
267+ return Ok((0, 0, 0, 0));
268+ #[cfg(unix)]
269+ controlling_window_size()
270+ }
271+
262272 pub fn graphics_size(&mut self) -> HostResult<(u16, u16, u16, u16)> {
263273 #[cfg(not(unix))]
264274 return Err(failure("terminal graphics is supported only on Unix"));
@@ -715,6 +725,41 @@ fn window_size() -> HostResult<(u16, u16, u16, u16)> {
715725 Ok((size.ws_col, size.ws_row, size.ws_xpixel, size.ws_ypixel))
716726}
717727
728+#[cfg(unix)]
729+fn controlling_window_size() -> HostResult<(u16, u16, u16, u16)> {
730+ let fd = unsafe {
731+ libc::open(
732+ b"/dev/tty\0".as_ptr().cast(),
733+ libc::O_RDONLY | libc::O_CLOEXEC,
734+ )
735+ };
736+ if fd < 0 {
737+ let error = io::Error::last_os_error();
738+ return if error.raw_os_error() == Some(libc::ENXIO) {
739+ Ok((0, 0, 0, 0))
740+ } else {
741+ Err(error.into())
742+ };
743+ }
744+ let mut size = std::mem::MaybeUninit::<libc::winsize>::zeroed();
745+ let result = unsafe { libc::ioctl(fd, libc::TIOCGWINSZ, size.as_mut_ptr()) };
746+ let ioctl_error = if result < 0 {
747+ Some(io::Error::last_os_error())
748+ } else {
749+ None
750+ };
751+ unsafe {
752+ libc::close(fd);
753+ }
754+ match ioctl_error {
755+ Some(error) => Err(error.into()),
756+ None => {
757+ let size = unsafe { size.assume_init() };
758+ Ok((size.ws_col, size.ws_row, size.ws_xpixel, size.ws_ypixel))
759+ }
760+ }
761+}
762+
718763#[cfg(unix)]
719764struct TtyInput {
720765 file: std::fs::File,
native/runtime/src/terminal_shm.rs+69−8
@@ -14,9 +14,9 @@ static NAME_SECRET: LazyLock<io::Result<[u8; 12]>> = LazyLock::new(|| {
1414 Ok(secret)
1515});
1616
17−/// A POSIX shared-memory object whose mapping remains valid until the terminal
18−/// acknowledges that it has read the frame. Dropping it always releases only
19−/// this process's mapping and attempts to unlink only this object's name.
17+/// A POSIX shared-memory object whose mapping remains valid until its owner
18+/// releases the frame. Dropping it always releases only this process's mapping
19+/// and attempts to unlink only this object's name.
2020pub(crate) struct SharedFrame {
2121 name: [u8; 29],
2222 mapping: *mut u8,
@@ -99,9 +99,27 @@ impl SharedFrame {
9999 &self.name[..28]
100100 }
101101
102+ /// Returns true only after another process has unlinked this frame's POSIX
103+ /// name. The mapping remains owned and usable until this frame is dropped.
104+ pub(crate) fn consumed(&self) -> io::Result<bool> {
105+ let fd = unsafe { libc::shm_open(self.name.as_ptr().cast(), libc::O_RDONLY, 0) };
106+ if fd >= 0 {
107+ unsafe {
108+ libc::close(fd);
109+ }
110+ return Ok(false);
111+ }
112+ let error = io::Error::last_os_error();
113+ if error.raw_os_error() == Some(libc::ENOENT) {
114+ Ok(true)
115+ } else {
116+ Err(error)
117+ }
118+ }
119+
102120 pub(crate) fn bytes_mut(&mut self) -> &mut [u8] {
103− // The mapping is exclusively owned by this frame. The terminal only
104− // receives its name after rendering has written every byte.
121+ // The mapping is exclusively owned by this frame. A consumer receives
122+ // its name only after rendering has written every byte.
105123 unsafe { std::slice::from_raw_parts_mut(self.mapping, self.length) }
106124 }
107125}
@@ -116,8 +134,8 @@ impl Drop for SharedFrame {
116134}
117135
118136fn unlink(name: &[u8; 29]) {
119− // The Kitty protocol requires the terminal to unlink the object after it
120− // has read it. ENOENT is therefore expected on the successful path.
137+ // A consumer may unlink the object after reading it. ENOENT is therefore
138+ // expected on a successful hand-off path.
121139 unsafe {
122140 libc::shm_unlink(name.as_ptr().cast());
123141 }
@@ -163,6 +181,8 @@ mod tests {
163181 unsafe { libc::fcntl(fd, libc::F_GETFD) } & libc::FD_CLOEXEC,
164182 0
165183 );
184+ assert!(!frame.consumed().unwrap());
185+
166186 unsafe { libc::close(fd) };
167187 drop(frame);
168188 assert_eq!(
@@ -176,11 +196,52 @@ mod tests {
176196 }
177197
178198 #[test]
179− fn terminal_unlink_keeps_sender_mapping_alive_until_drop() {
199+ fn terminal_unlink_marks_frame_consumed_without_releasing_mapping() {
180200 let mut frame = SharedFrame::create(4).unwrap();
181201 frame.bytes_mut().copy_from_slice(&[21, 42, 63, 127]);
182202 assert_eq!(unsafe { libc::shm_unlink(frame.name.as_ptr().cast()) }, 0);
203+ assert!(frame.consumed().unwrap());
183204 assert_eq!(frame.bytes_mut(), &[21, 42, 63, 127]);
184205 drop(frame);
185206 }
207+
208+ #[test]
209+ fn consumed_propagates_non_missing_shared_memory_errors() {
210+ const CHILD: &str = "TAURI_TEST_SHARED_FRAME_FD_LIMIT";
211+ if std::env::var_os(CHILD).is_some() {
212+ let frame = SharedFrame::create(4).unwrap();
213+ let mut previous = std::mem::MaybeUninit::<libc::rlimit>::uninit();
214+ assert_eq!(
215+ unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, previous.as_mut_ptr()) },
216+ 0
217+ );
218+ let previous = unsafe { previous.assume_init() };
219+ let exhausted = libc::rlimit {
220+ rlim_cur: 0,
221+ rlim_max: previous.rlim_max,
222+ };
223+ assert_eq!(
224+ unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &exhausted) },
225+ 0
226+ );
227+ let result = frame.consumed();
228+ assert_eq!(
229+ unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &previous) },
230+ 0
231+ );
232+ assert_eq!(result.unwrap_err().raw_os_error(), Some(libc::EMFILE));
233+ assert!(!frame.consumed().unwrap());
234+ return;
235+ }
236+ // Descriptor exhaustion must not perturb other tests running in parallel.
237+ let status = std::process::Command::new(std::env::current_exe().unwrap())
238+ .args([
239+ "--exact",
240+ "terminal_shm::tests::consumed_propagates_non_missing_shared_memory_errors",
241+ ])
242+ .env(CHILD, "1")
243+ .status()
244+ .unwrap();
245+ assert!(status.success());
246+ }
186247}
src/Tauri/Native.js+1
@@ -9,6 +9,7 @@ export { nativeArguments as arguments };
99export const version = () => host().call("version");
1010export const isStdinTTY = () => host().call("isStdinTTY");
1111export const readStdin = () => host().call("readStdin");
12+export const readLineImpl = () => host().call("readLine");
1213export const readText = path => () => host().call("readText", path);
1314export const writeText = path => text => () => { host().call("writeText", path, text); };
1415export const stdout = text => () => { host().call("stdout", text); };
src/Tauri/Native.purs+8−1
@@ -8,6 +8,7 @@ module Tauri.Native
88 , version
99 , isStdinTTY
1010 , readStdin
11+ , readLine
1112 , readText
1213 , writeText
1314 , stdout
@@ -33,7 +34,7 @@ module Tauri.Native
3334import Prelude
3435
3536import Data.Maybe (Maybe)
36−import Data.Nullable (Nullable, toNullable)
37+import Data.Nullable (Nullable, toMaybe, toNullable)
3738import Effect (Effect)
3839import Tauri.Native.Canvas (Surface)
3940
@@ -56,12 +57,18 @@ runApplicationTask args input = runApplicationTaskImpl args (toNullable input)
5657runApplication :: Array String -> Maybe String -> Effect ProcessResult
5758runApplication args input = runApplicationImpl args (toNullable input)
5859
60+-- | Read one UTF-8 line from stdin, without consuming any following input.
61+-- | Returns `Nothing` only at EOF.
62+readLine :: Effect (Maybe String)
63+readLine = toMaybe <$> readLineImpl
64+
5965foreign import data Video :: Type
6066
6167foreign import arguments :: Effect (Array String)
6268foreign import version :: Effect String
6369foreign import isStdinTTY :: Effect Boolean
6470foreign import readStdin :: Effect String
71+foreign import readLineImpl :: Effect (Nullable String)
6572foreign import readText :: String -> Effect String
6673foreign import writeText :: String -> String -> Effect Unit
6774foreign import stdout :: String -> Effect Unit
src/Tauri/Native/Canvas.js+1−1
@@ -4,7 +4,7 @@ const host = () => {
44 return native;
55};
66
7−export const create = size => () => host().call("canvasCreate", size);
7+export const createImpl = backend => size => () => host().call("canvasCreate", backend, size);
88export const release = target => () => { host().call("canvasRelease", target); };
99export const reset = target => () => { host().call("canvasReset", target); };
1010export const clear = target => color => () => { host().call("canvasClear", target, color); };
src/Tauri/Native/Canvas.purs+11−3
@@ -1,9 +1,9 @@
11-- | Reusable Skia canvases. Requires tauri-native-host.
2−-- | Normally CPU-backed; canvases created during a macOS terminal graphics
3−-- | session use Metal instead.
2+-- | Each canvas explicitly selects its CPU or macOS Metal backend.
43-- | Surface handles must be released when their owner is finished drawing.
54module Tauri.Native.Canvas
65 ( Surface
6+ , Backend(..)
77 , Color
88 , Affine
99 , Rect
@@ -38,6 +38,14 @@ import Data.Maybe (Maybe)
3838import Data.Nullable (Nullable, toNullable)
3939import Effect (Effect)
4040
41+data Backend = Cpu | Metal
42+
43+-- | Metal allocation fails explicitly when the platform or device is unavailable.
44+create :: Backend -> { width :: Int, height :: Int } -> Effect Surface
45+create backend = createImpl case backend of
46+ Cpu -> "cpu"
47+ Metal -> "metal"
48+
4149-- | Straight (unpremultiplied) RGBA channels in 0..255.
4250type Color = { r :: Int, g :: Int, b :: Int, a :: Int }
4351
@@ -82,7 +90,7 @@ setTint target color = setTintImpl target (toNullable color)
8290
8391foreign import data Surface :: Type
8492
85−foreign import create :: { width :: Int, height :: Int } -> Effect Surface
93+foreign import createImpl :: String -> { width :: Int, height :: Int } -> Effect Surface
8694foreign import release :: Surface -> Effect Unit
8795-- | Clear transparent and reset transform, clipping, styles and save stack.
8896foreign import reset :: Surface -> Effect Unit
src/Tauri/Native/SharedFrame.jsnew file+12
@@ -0,0 +1,12 @@
1+const host = () => {
2+ const native = globalThis.__purescriptNativeHost;
3+ if (!native) throw new Error("Tauri.Native.SharedFrame requires tauri-native-host");
4+ return native;
5+};
6+
7+export const open = () => { host().call("sharedFrameOpen"); };
8+export const capture = surface => () => host().call("sharedFrameCapture", surface);
9+export const info = frame => host().call("sharedFrameInfo", frame);
10+export const consumed = frame => () => host().call("sharedFrameConsumed", frame);
11+export const release = frame => () => { host().call("sharedFrameRelease", frame); };
12+export const close = () => { host().call("sharedFrameClose"); };
src/Tauri/Native/SharedFrame.pursnew file+41
@@ -0,0 +1,41 @@
1+-- | POSIX shared-memory RGBA frames backed by the native Metal canvas renderer.
2+module Tauri.Native.SharedFrame
3+ ( Frame
4+ , open
5+ , capture
6+ , info
7+ , consumed
8+ , release
9+ , close
10+ ) where
11+
12+import Prelude (Unit)
13+
14+import Effect (Effect)
15+import Tauri.Native.Canvas (Surface)
16+
17+foreign import data Frame :: Type
18+
19+-- | Open frame ownership and verify Metal without interacting with a terminal.
20+-- | Canvas allocation selects its backend explicitly.
21+foreign import open :: Effect Unit
22+
23+-- | Read a canvas's straight RGBA pixels directly into an owned shared mapping.
24+foreign import capture :: Surface -> Effect Frame
25+
26+-- | Metadata for an open frame. `name` is the POSIX name, including its leading
27+-- | slash. `byteLength` is the tight RGBA length, excluding any POSIX page padding.
28+foreign import info
29+ :: Frame
30+ -> { id :: Int, name :: String, width :: Int, height :: Int, byteLength :: Int }
31+
32+-- | Whether the terminal has unlinked this frame's POSIX name. This checks only
33+-- | the named object: it neither copies pixels nor releases the owned mapping.
34+-- | Non-missing shared-memory errors are raised.
35+foreign import consumed :: Frame -> Effect Boolean
36+
37+-- | Release this frame's mapping. Releasing an already released frame is safe.
38+foreign import release :: Frame -> Effect Unit
39+
40+-- | Release every remaining frame. Existing canvases retain their own backend.
41+foreign import close :: Effect Unit
src/Tauri/Native/TerminalGraphics.js+1
@@ -4,6 +4,7 @@ const host = () => {
44 return native;
55};
66
7+export const inspectSize = () => host().call("terminalGraphicsInspectSize");
78export const open = () => { host().call("terminalGraphicsOpen"); };
89export const size = () => host().call("terminalGraphicsSize");
910export const present = surface => position => () => {
src/Tauri/Native/TerminalGraphics.purs+9−2
@@ -1,7 +1,8 @@
11-- | Kitty graphics terminal session backed by tauri-native-host.
22-- | Call `close` exactly once after `open`, including exceptional exits.
33module Tauri.Native.TerminalGraphics
4− ( open
4+ ( inspectSize
5+ , open
56 , size
67 , present
78 , readKey
@@ -13,8 +14,14 @@ import Prelude (Unit)
1314import Effect (Effect)
1415import Tauri.Native.Canvas (Surface)
1516
17+-- | Read the controlling terminal's ioctl dimensions without reading input,
18+-- | changing terminal mode, or writing terminal escapes. Returns zeros when no
19+-- | controlling terminal exists; reported nonzero values are never inferred.
20+foreign import inspectSize :: Effect { columns :: Int, rows :: Int, width :: Int, height :: Int }
21+
1622-- | Require a usable TTY, verify Kitty graphics support, then acquire a
17−-- | fullscreen terminal session with Metal-backed canvases on macOS.
23+-- | fullscreen terminal session and verify Metal availability on macOS.
24+-- | Canvas allocation selects its backend explicitly.
1825-- | Throws when the protocol or GPU backend is unavailable.
1926foreign import open :: Effect Unit
2027