1 /++
2 	$(PITFALL
3 		Please note: the api and behavior of this module is not externally stable at this time. See the documentation on specific functions for details.
4 	)
5 
6 	Shared core functionality including exception helpers, library loader, event loop, and possibly more. Maybe command line processor and uda helper and some basic shared annotation types.
7 
8 	I'll probably move the url, websocket, and ssl stuff in here too as they are often shared. Maybe a small internationalization helper type (a hook for external implementation) and COM helpers too. I might move the process helpers out to their own module - even things in here are not considered stable to library users at this time!
9 
10 	If you use this directly outside the arsd library despite its current instability caveats, you might consider using `static import` since names in here are likely to clash with Phobos if you use them together. `static import` will let you easily disambiguate and avoid name conflict errors if I add more here. Some names even clash deliberately to remind me to avoid some antipatterns inside the arsd modules!
11 
12 	## Contributor notes
13 
14 	arsd.core should be focused on things that enable interoperability primarily and secondarily increased code quality between other, otherwise independent arsd modules. As a foundational library, it is not permitted to import anything outside the druntime `core` namespace, except in templates and examples not normally compiled in. This keeps it independent and avoids transitive dependency spillover to end users while also keeping compile speeds fast. To help keep builds snappy, also avoid significant use of ctfe inside this module.
15 
16 	On my linux computer, `dmd -unittest -main core.d` takes about a quarter second to run. We do not want this to grow.
17 
18 	`@safe` compatibility is ok when it isn't too big of a hassle. `@nogc` is a non-goal. I might accept it on some of the trivial functions but if it means changing the logic in any way to support, you will need a compelling argument to justify it. The arsd libs are supposed to be reliable and easy to use. That said, of course, don't be unnecessarily wasteful - if you can easily provide a reliable and easy to use way to let advanced users do their thing without hurting the other cases, let's discuss it.
19 
20 	If functionality is not needed by multiple existing arsd modules, consider adding a new module instead of adding it to the core.
21 
22 	Unittests should generally be hidden behind a special version guard so they don't interfere with end user tests.
23 
24 	History:
25 		Added March 2023 (dub v11.0). Several functions were migrated in here at that time, noted individually. Members without a note were added with the module.
26 +/
27 module arsd.core;
28 
29 /+
30 	Intended to be Supported OSes:
31 		* Windows (at least Vista, MAYBE XP)
32 		* Linux
33 		* FreeBSD 14 (maybe 13 too)
34 		* Mac OS
35 
36 	Eventually also:
37 		* ios
38 		* OpenBSD
39 		* Android
40 		* maybe apple watch os?
41 +/
42 
43 
44 static if(__traits(compiles, () { import core.interpolation; })) {
45 	import core.interpolation;
46 
47 	alias InterpolationHeader    = core.interpolation.InterpolationHeader;
48 	alias InterpolationFooter    = core.interpolation.InterpolationFooter;
49 	alias InterpolatedLiteral    = core.interpolation.InterpolatedLiteral;
50 	alias InterpolatedExpression = core.interpolation.InterpolatedExpression;
51 } else {
52 	// polyfill for old versions
53 	struct InterpolationHeader {}
54 	struct InterpolationFooter {}
55 	struct InterpolatedLiteral(string literal) {}
56 	struct InterpolatedExpression(string code) {}
57 }
58 
59 static if(!__traits(hasMember, object, "SynchronizableObject")) {
60 	alias SynchronizableObject = Object;
61 	mixin template EnableSynchronization() {}
62 } else {
63 	alias SynchronizableObject = object.SynchronizableObject;
64 	alias EnableSynchronization = Object.EnableSynchronization;
65 }
66 
67 // the old char.inits if you need them
68 enum  char  char_invalid = '\xFF';
69 enum wchar wchar_invalid = '\uFFFF';
70 enum dchar dchar_invalid = '\U0000FFFF';
71 
72 // arsd core is now default but you can opt out for a lil while
73 version(no_arsd_core) {
74 
75 } else {
76 	version=use_arsd_core;
77 }
78 
79 version(use_arsd_core)
80 	enum use_arsd_core = true;
81 else
82 	enum use_arsd_core = false;
83 
84 import core.attribute;
85 static if(__traits(hasMember, core.attribute, "implicit"))
86 	alias implicit = core.attribute.implicit;
87 else
88 	enum implicit;
89 
90 static if(__traits(hasMember, core.attribute, "standalone"))
91 	alias standalone = core.attribute.standalone;
92 else
93 	enum standalone;
94 
95 
96 
97 // FIXME: add callbacks on file open for tracing dependencies dynamically
98 
99 // see for useful info: https://devblogs.microsoft.com/dotnet/how-async-await-really-works/
100 
101 // see: https://wiki.openssl.org/index.php/Simple_TLS_Server
102 
103 // see: When you only want to track changes on a file or directory, be sure to open it using the O_EVTONLY flag.
104 
105 ///ArsdUseCustomRuntime is used since other derived work from WebAssembly may be used and thus specified in the CLI
106 version(Emscripten) {
107 	version = EmptyEventLoop;
108 	version = EmptyCoreEvent;
109 	version = HasTimer;
110 } else version(WebAssembly) version = ArsdUseCustomRuntime;
111 else
112 
113 // note that kqueue might run an i/o loop on mac, ios, etc. but then NSApp.run on the io thread
114 // but on bsd, you want the kqueue loop in i/o too....
115 
116 version(ArsdUseCustomRuntime)
117 {
118 	version = UseStdioWriteln;
119 }
120 else
121 {
122 	version(ArsdNoCocoa) {
123 	} else {
124 		version(D_OpenD) {
125 			version(OSX)
126 				version=OSXCocoa;
127 			version(iOS)
128 				version=OSXCocoa;
129 		} else version(DigitalMars) {
130 			version(OSX)
131 				version=OSXCocoa;
132 			version(iOS)
133 				version=OSXCocoa;
134 		} else version(LDC) {
135 			version(OSX)
136 				version=OSXCocoa;
137 			version(iOS)
138 				version=OSXCocoa;
139 		}
140 	}
141 
142 	version = HasFile;
143 	version = HasSocket;
144 	version = HasThread;
145 	import core.stdc.errno;
146 
147 	version(Windows)
148 		version = HasTimer;
149 	version(linux)
150 		version = HasTimer;
151 	version(OSX)
152 		version = HasTimer;
153 }
154 
155 version(HasThread)
156 {
157 	import core.thread;
158 	import core..volatile;
159 	import core.atomic;
160 }
161 else
162 {
163 	// polyfill for missing core.time
164 	/*
165 	struct Duration {
166 		static Duration max() { return Duration(); }
167 	}
168 	struct MonoTime {}
169 	*/
170 }
171 
172 import core.time;
173 
174 version(OSXCocoa) {
175 	version(ArsdNoCocoa)
176 		enum bool UseCocoa = false;
177 	else {
178 		version=UseCocoa;
179 		enum bool UseCocoa = true;
180 	}
181 } else
182 	enum bool UseCocoa = false;
183 
184 import core.attribute;
185 static if(!__traits(hasMember, core.attribute, "mustuse"))
186 	enum mustuse;
187 
188 // FIXME: add an arena allocator? can do task local destruction maybe.
189 
190 // the three implementations are windows, epoll, and kqueue
191 
192 version(Emscripten)  {
193 	import core.stdc.errno;
194 	import core.atomic;
195 	import core..volatile;
196 
197 } else version(Windows) {
198 	version=Arsd_core_windows;
199 
200 	// import core.sys.windows.windows;
201 	import core.sys.windows.winbase;
202 	import core.sys.windows.windef;
203 	import core.sys.windows.winnls;
204 	import core.sys.windows.winuser;
205 	import core.sys.windows.winsock2;
206 
207 	pragma(lib, "user32");
208 	pragma(lib, "ws2_32");
209 } else version(linux) {
210 	version=Arsd_core_epoll;
211 
212 	static if(__VERSION__ >= 2098) {
213 		version=Arsd_core_has_cloexec;
214 	}
215 } else version(FreeBSD) {
216 	version=Arsd_core_kqueue;
217 
218 	import core.sys.freebsd.sys.event;
219 
220 	// the version in druntime doesn't have the default arg making it a pain to use when the freebsd
221 	// version adds a new field
222 	extern(D) void EV_SET(kevent_t* kevp, typeof(kevent_t.tupleof) args = kevent_t.tupleof.init)
223 	{
224 	    *kevp = kevent_t(args);
225 	}
226 } else version(DragonFlyBSD) {
227 	// NOT ACTUALLY TESTED
228 	version=Arsd_core_kqueue;
229 
230 	import core.sys.dragonflybsd.sys.event;
231 } else version(NetBSD) {
232 	// NOT ACTUALLY TESTED
233 	version=Arsd_core_kqueue;
234 
235 	import core.sys.netbsd.sys.event;
236 } else version(OpenBSD) {
237 	version=Arsd_core_kqueue;
238 
239 	// THIS FILE DOESN'T ACTUALLY EXIST, WE NEED TO MAKE IT
240 	import core.sys.openbsd.sys.event;
241 } else version(OSX) {
242 	version(ArsdNoCocoa) {
243 		version=Arsd_core_kqueue;
244 	} else {
245 		version=Arsd_core_dispatch;
246 	}
247 
248 	import core.sys.darwin.sys.event;
249 } else version(iOS) {
250 	version=Arsd_core_dispatch;
251 
252 	import core.sys.darwin.sys.event;
253 }
254 
255 // FIXME: pragma(linkerDirective, "-framework", "Cocoa") works in ldc
256 static if(UseCocoa)
257 	enum CocoaAvailable = true;
258 else
259 	enum CocoaAvailable = false;
260 
261 version(D_OpenD) {
262 	static if(UseCocoa) {
263 		pragma(linkerDirective, "-framework", "Cocoa");
264 		pragma(linkerDirective, "-framework", "QuartzCore");
265 	}
266 } else {
267 	static if(UseCocoa)
268 	version(LDC) {
269 		pragma(linkerDirective, "-framework", "Cocoa");
270 		pragma(linkerDirective, "-framework", "QuartzCore");
271 	}
272 }
273 
274 version(Posix) {
275 	import core.sys.posix.signal;
276 	import core.sys.posix.unistd;
277 
278 	version(Emscripten) {} else {
279 	import core.sys.posix.sys.un;
280 	import core.sys.posix.sys.socket;
281 	import core.sys.posix.netinet.in_;
282 	}
283 }
284 
285 // FIXME: the exceptions should actually give some explanatory text too (at least sometimes)
286 
287 /+
288 	=========================
289 	GENERAL UTILITY FUNCTIONS
290 	=========================
291 +/
292 
293 /++
294 	Casts value `v` to type `T`.
295 
296 	$(TIP
297 		This is a helper function for readability purposes.
298 		The idea is to make type-casting as accessible as `to()` from `std.conv`.
299 	)
300 
301 	---
302 	int i =  cast(int)(foo * bar);
303 	int i = castTo!int(foo * bar);
304 
305 	int j = cast(int) round(floatValue);
306 	int j = round(floatValue).castTo!int;
307 
308 	int k = cast(int) floatValue  + foobar;
309 	int k = floatValue.castTo!int + foobar;
310 
311 	auto m = Point(
312 		cast(int) calc(a.x, b.x),
313 		cast(int) calc(a.y, b.y),
314 	);
315 	auto m = Point(
316 		calc(a.x, b.x).castTo!int,
317 		calc(a.y, b.y).castTo!int,
318 	);
319 	---
320 
321 	History:
322 		Added on April 24, 2024.
323 		Renamed from `typeCast` to `castTo` on May 24, 2024.
324  +/
325 auto ref T castTo(T, S)(auto ref S v) {
326 	return cast(T) v;
327 }
328 
329 ///
330 alias typeCast = castTo;
331 
332 /++
333 	Treats the memory of one variable as if it is the type of another variable.
334 
335 	History:
336 		Added January 20, 2025
337 +/
338 ref T reinterpretCast(T, V)(return ref V value) @system {
339 	return *cast(T*)& value;
340 }
341 
342 /++
343 	Determines whether `needle` is a slice of `haystack`.
344 
345 	History:
346 		Added on February 11, 2025.
347  +/
348 bool isSliceOf(T1, T2)(scope const(T1)[] needle, scope const(T2)[] haystack) @trusted pure nothrow @nogc {
349 	return (
350 		needle.ptr >= haystack.ptr
351 		&& ((needle.ptr + needle.length) <= (haystack.ptr + haystack.length))
352 	);
353 }
354 
355 ///
356 @safe unittest {
357 	string        s0 = "01234";
358 	const(char)[] s1 = s0[1 .. $];
359 	const(void)[] s2 = s1.castTo!(const(void)[]);
360 	string        s3 = s1.idup;
361 
362 	assert( s0.isSliceOf(s0));
363 	assert( s1.isSliceOf(s0));
364 	assert( s2.isSliceOf(s0));
365 	assert(!s3.isSliceOf(s0));
366 
367 	assert(!s0.isSliceOf(s1));
368 	assert( s1.isSliceOf(s1));
369 	assert( s2.isSliceOf(s1));
370 	assert(!s3.isSliceOf(s1));
371 
372 	assert(!s0.isSliceOf(s2));
373 	assert( s1.isSliceOf(s2));
374 	assert( s2.isSliceOf(s2));
375 	assert(!s3.isSliceOf(s2));
376 
377 	assert(!s0.isSliceOf(s3));
378 	assert(!s1.isSliceOf(s3));
379 	assert(!s2.isSliceOf(s3));
380 	assert( s3.isSliceOf(s3));
381 
382 	assert(s1.length == 4);
383 	assert(s1[0 .. 0].isSliceOf(s1));
384 	assert(s1[0 .. 1].isSliceOf(s1));
385 	assert(s1[1 .. 2].isSliceOf(s1));
386 	assert(s1[1 .. 3].isSliceOf(s1));
387 	assert(s1[1 .. $].isSliceOf(s1));
388 	assert(s1[$ .. $].isSliceOf(s1));
389 }
390 
391 /++
392 	Does math as a 64 bit number, but saturates at int.min and int.max when converting back to a 32 bit int.
393 
394 	History:
395 		Added January 1, 2025
396 +/
397 alias NonOverflowingInt = NonOverflowingIntBase!(int.min, int.max);
398 
399 /// ditto
400 alias NonOverflowingUint = NonOverflowingIntBase!(0, int.max);
401 
402 /// ditto
403 struct NonOverflowingIntBase(int min, int max) {
404 	this(long v) {
405 		this.value = v;
406 	}
407 
408 	private long value;
409 
410 	NonOverflowingInt opBinary(string op)(long rhs) {
411 		return NonOverflowingInt(mixin("this.value", op, "rhs"));
412 	}
413 	NonOverflowingInt opBinary(string op)(NonOverflowingInt rhs) {
414 		return this.opBinary!op(rhs.value);
415 	}
416 	NonOverflowingInt opUnary(string op)() {
417 		return NonOverflowingInt(mixin(op, "this.value"));
418 	}
419 	NonOverflowingInt opOpAssign(string op)(long rhs) {
420 		return this = this.opBinary!(op)(rhs);
421 	}
422 	NonOverflowingInt opOpAssign(string op)(NonOverflowingInt rhs) {
423 		return this = this.opBinary!(op)(rhs.value);
424 	}
425 
426 	int getValue() const {
427 		if(value < min)
428 			return min;
429 		else if(value > max)
430 			return max;
431 		return cast(int) value;
432 	}
433 
434 	alias getValue this;
435 }
436 
437 unittest {
438 	assert(-5.NonOverflowingInt - int.max == int.min);
439 	assert(-5.NonOverflowingInt + 5 == 0);
440 
441 	assert(NonOverflowingInt(5) + int.max - 5 == int.max);
442 	assert(NonOverflowingInt(5) + int.max - int.max - 5 == 0); // it truncates at the end of the op chain, not at intermediates
443 	assert(NonOverflowingInt(0) + int.max * 2L == int.max); // note the L there is required to pass since the order of operations means mul done before it gets to the NonOverflowingInt controls
444 }
445 
446 // enum stringz : const(char)* { init = null }
447 
448 /++
449 	A wrapper around a `const(char)*` to indicate that it is a zero-terminated C string.
450 +/
451 struct stringz {
452 	private const(char)* raw;
453 
454 	/++
455 		Wraps the given pointer in the struct. Note that it retains a copy of the pointer.
456 	+/
457 	this(const(char)* raw) {
458 		this.raw = raw;
459 	}
460 
461 	/++
462 		Returns the original raw pointer back out.
463 	+/
464 	const(char)* ptr() const {
465 		return raw;
466 	}
467 
468 	/++
469 		Borrows a slice of the pointer up to (but not including) the zero terminator.
470 	+/
471 	const(char)[] borrow() const @system {
472 		if(raw is null)
473 			return null;
474 
475 		const(char)* p = raw;
476 		int length;
477 		while(*p++) length++;
478 
479 		return raw[0 .. length];
480 	}
481 }
482 
483 /+
484 /++
485 	A runtime tagged union, aka a sumtype.
486 
487 	History:
488 		Added February 15, 2025
489 +/
490 struct Union(T...) {
491 	private uint contains_;
492 	private union {
493 		private T payload;
494 	}
495 
496 	static foreach(index, type; T)
497 	@implicit public this(type t) {
498 		contains_ = index;
499 		payload[index] = t;
500 	}
501 
502 	bool contains(Part)() const {
503 		static assert(indexFor!Part != -1);
504 		return contains_ == indexFor!Part;
505 	}
506 
507 	inout(Part) get(Part)() inout {
508 		if(!contains!Part) {
509 			throw new ArsdException!"Dynamic type mismatch"(indexFor!Part, contains_);
510 		}
511 		return payload[indexFor!Part];
512 	}
513 
514 	private int indexFor(Part)() {
515 		foreach(idx, thing; T)
516 			static if(is(T == Part))
517 				return idx;
518 		return -1;
519 	}
520 }
521 +/
522 
523 /+
524 	DateTime
525 		year: 16 bits (-32k to +32k)
526 		month: 4 bits
527 		day: 5 bits
528 
529 		hour: 5 bits
530 		minute: 6 bits
531 		second: 6 bits
532 
533 		total: 25 bits + 17 bits = 42 bits
534 
535 		fractional seconds: 10 bits (about milliseconds)
536 
537 		accuracy flags: date_valid | time_valid = 2 bits
538 
539 		54 bits used, 8 bits remain. reserve 1 for signed.
540 
541 		tz offset in 15 minute intervals = 96 slots... can fit in 7 remaining bits...
542 
543 		would need 11 bits for minute-precise dt offset but meh. would need 10 bits for referring back to tz database (and that's iffy to key, better to use a string tbh)
544 +/
545 
546 /++
547 	A packed date/time/datetime representation added for use with LimitedVariant.
548 
549 	You should probably not use this much directly, it is mostly an internal storage representation.
550 +/
551 struct PackedDateTime {
552 	private ulong packedData;
553 
554 	string toString() const {
555 		char[64] buffer;
556 		size_t pos;
557 
558 		if(hasDate) {
559 			pos += intToString(year, buffer[pos .. $], IntToStringArgs().withPadding(4)).length;
560 			buffer[pos++] = '-';
561 			pos += intToString(month, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
562 			buffer[pos++] = '-';
563 			pos += intToString(day, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
564 		}
565 
566 		if(hasTime) {
567 			if(pos)
568 				buffer[pos++] = 'T';
569 
570 			pos += intToString(hours, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
571 			buffer[pos++] = ':';
572 			pos += intToString(minutes, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
573 			buffer[pos++] = ':';
574 			pos += intToString(seconds, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
575 			if(fractionalSeconds) {
576 				buffer[pos++] = '.';
577 				pos += intToString(fractionalSeconds, buffer[pos .. $], IntToStringArgs().withPadding(4)).length;
578 			}
579 		}
580 
581 		return buffer[0 .. pos].idup;
582 	}
583 
584 	/++
585 		Construction helpers
586 	+/
587 	static PackedDateTime withDate(int year, int month, int day) {
588 		PackedDateTime p;
589 		p.setDate(year, month, day);
590 		return p;
591 	}
592 	/// ditto
593 	static PackedDateTime withTime(int hours, int minutes, int seconds, int fractionalSeconds = 0) {
594 		PackedDateTime p;
595 		p.setTime(hours, minutes, seconds, fractionalSeconds);
596 		return p;
597 	}
598 	/// ditto
599 	static PackedDateTime withDateAndTime(int year, int month, int day = 1, int hours = 0, int minutes = 0, int seconds = 0, int fractionalSeconds = 0) {
600 		PackedDateTime p;
601 		p.setDate(year, month, day);
602 		p.setTime(hours, minutes, seconds, fractionalSeconds);
603 		return p;
604 	}
605 	/// ditto
606 	static PackedDateTime lastDayOfMonth(int year, int month) {
607 		PackedDateTime p;
608 		p.setDate(year, month, daysInMonth(year, month));
609 		return p;
610 	}
611 	/++ +/
612 	static bool isLeapYear(int year) {
613 		return
614 			(year % 4) == 0
615 			&&
616 			(
617 				((year % 100) != 0)
618 				||
619 				((year % 400) == 0)
620 			)
621 		;
622 	}
623 	unittest {
624 		assert(isLeapYear(2024));
625 		assert(!isLeapYear(2023));
626 		assert(!isLeapYear(2025));
627 		assert(isLeapYear(2000));
628 		assert(!isLeapYear(1900));
629 	}
630 	static immutable ubyte[12] daysInMonthTable = [
631 		31, 28, 31, 30, 31, 30,
632 		31, 31, 30, 31, 30, 31
633 	];
634 
635 	static int daysInMonth(int year, int month) {
636 		assert(month >= 1 &&  month <= 12);
637 		if(month == 2)
638 			return isLeapYear(year) ? 29 : 28;
639 		else
640 			return daysInMonthTable[month - 1];
641 	}
642 	unittest {
643 		assert(daysInMonth(2025, 12) == 31);
644 		assert(daysInMonth(2025, 2) == 28);
645 		assert(daysInMonth(2024, 2) == 29);
646 	}
647 	static int daysInYear(int year) {
648 		return isLeapYear(year) ? 366 : 365;
649 	}
650 
651 	/++
652 		Sets the whole date and time portions in one function call.
653 
654 		History:
655 			Added December 13, 2025
656 	+/
657 	void setTime(int hours, int minutes, int seconds, int fractionalSeconds = 0) {
658 		this.hours = hours;
659 		this.minutes = minutes;
660 		this.seconds = seconds;
661 		this.fractionalSeconds = fractionalSeconds;
662 		this.hasTime = true;
663 	}
664 
665 	/// ditto
666 	void setDate(int year, int month, int day) {
667 		this.year = year;
668 		this.month = month;
669 		this.day = day;
670 		this.hasDate = true;
671 	}
672 
673 	/// ditto
674 	void clearTime() {
675 		this.hours = 0;
676 		this.minutes = 0;
677 		this.seconds = 0;
678 		this.fractionalSeconds = 0;
679 		this.hasTime = false;
680 	}
681 
682 	/// ditto
683 	void clearDate() {
684 		this.year = 0;
685 		this.month = 0;
686 		this.day = 0;
687 		this.hasDate = false;
688 	}
689 
690 	/++
691 	+/
692 	int fractionalSeconds() const { return getFromMask(00, 10); }
693 	/// ditto
694 	void fractionalSeconds(int a) {     setWithMask(a, 00, 10); }
695 
696 	/// ditto
697 	int  seconds() const          { return getFromMask(10,  6); }
698 	/// ditto
699 	void seconds(int a)           {     setWithMask(a, 10,  6); }
700 	/// ditto
701 	int  minutes() const          { return getFromMask(16,  6); }
702 	/// ditto
703 	void minutes(int a)           {     setWithMask(a, 16,  6); }
704 	/// ditto
705 	int  hours() const            { return getFromMask(22,  5); }
706 	/// ditto
707 	void hours(int a)             {     setWithMask(a, 22,  5); }
708 
709 	/// ditto
710 	int  day() const              { return getFromMask(27,  5); }
711 	/// ditto
712 	void day(int a)               {     setWithMask(a, 27,  5); }
713 	/// ditto
714 	int  month() const            { return getFromMask(32,  4); }
715 	/// ditto
716 	void month(int a)             {     setWithMask(a, 32,  4); }
717 	/// ditto
718 	int  year() const             { return getFromMask(36, 16); }
719 	/// ditto
720 	void year(int a)              {     setWithMask(a, 36, 16); }
721 
722 	/// ditto
723 	bool hasTime() const          { return cast(bool) getFromMask(52,  1); }
724 	/// ditto
725 	void hasTime(bool a)          {     setWithMask(a, 52,  1); }
726 	/// ditto
727 	bool hasDate() const          { return cast(bool) getFromMask(53,  1); }
728 	/// ditto
729 	void hasDate(bool a)          {     setWithMask(a, 53,  1); }
730 
731 	private void setWithMask(int a, int bitOffset, int bitCount) {
732 		auto mask = (1UL << bitCount) - 1;
733 
734 		packedData &= ~(mask << bitOffset);
735 		packedData |= (a & mask) << bitOffset;
736 	}
737 
738 	private int getFromMask(int bitOffset, int bitCount) const {
739 		ulong packedData = this.packedData;
740 		packedData >>= bitOffset;
741 
742 		ulong mask = (1UL << bitCount) - 1;
743 
744 		return cast(int) (packedData & mask);
745 	}
746 
747 	/++
748 		Returns the day of week for the date portion.
749 
750 		Throws AssertError if used when [hasDate] is false.
751 
752 		Returns:
753 			0 == Sunday, 6 == Saturday
754 
755 		History:
756 			Added December 13, 2025
757 	+/
758 	int dayOfWeek() const {
759 		assert(hasDate);
760 		auto y = year;
761 		auto m = month;
762 		if(m == 1 || m == 2) {
763 			y--;
764 			m += 12;
765 		}
766 		return (
767 			day +
768 			(13 * (m+1) / 5) +
769 			(y % 100) +
770 			(y % 100) / 4 +
771 			(y / 100) / 4 -
772 			2 * (y / 100)
773 		) % 7;
774 	}
775 
776 	long opCmp(PackedDateTime rhs) const {
777 		if(this.hasDate == rhs.hasDate && this.hasTime == rhs.hasTime)
778 			return cast(long) this.packedData - cast(long) rhs.packedData;
779 		if(this.hasDate && rhs.hasDate) {
780 			PackedDateTime c1 = this;
781 			c1.clearTime();
782 			rhs.clearTime();
783 			return c1.opCmp(rhs);
784 		}
785 		// if one of them is just time, no date, we can't compare
786 		// but as long as there's two date components we can compare them.
787 		assert(0, "invalid comparison, one is a date, other is a time");
788 	}
789 }
790 
791 unittest {
792 	PackedDateTime dt;
793 	dt.hours = 14;
794 	dt.minutes = 30;
795 	dt.seconds = 25;
796 	dt.hasTime = true;
797 
798 	assert(dt.toString() == "14:30:25", dt.toString());
799 
800 	dt.hasTime = false;
801 	dt.year = 2024;
802 	dt.month = 5;
803 	dt.day = 31;
804 	dt.hasDate = true;
805 
806 	assert(dt.toString() == "2024-05-31", dt.toString());
807 	dt.hasTime = true;
808 	assert(dt.toString() == "2024-05-31T14:30:25", dt.toString());
809 
810 	assert(dt.dayOfWeek == 6);
811 }
812 
813 unittest {
814 	PackedDateTime a;
815 	PackedDateTime b;
816 	a.setDate(2025, 01, 01);
817 	b.setDate(2024, 12, 31);
818 	assert(a > b);
819 }
820 
821 /++
822 	A `PackedInterval` can be thought of as the difference between [PackedDateTime]s, similarly to how a [Duration] is a difference between [MonoTime]s or [SimplifiedUtcTimestamp]s.
823 
824 
825 	The key speciality is in how it treats months and days separately. Months are not a consistent length, and neither are days when you consider daylight saving time. This thing assumes that if you add those, the month/day number will always increase, just the exact details since then might be truncated. (E.g., January 31st + 1 month = February 28/29 depending on leap year). If you multiply, the parts are done individually, so January 31st + 1 month * 2 = March 31st, despite + 1 month truncating to the shorter day in February.
826 
827 	Internally, this stores months and days as 16 bit signed `short`s each, then the milliseconds is stored as a 32 bit signed `int`. It applies by first adding months, truncating days as needed, then adding days, then adding milliseconds.
828 
829 	If you iterate over intervals, be careful not to allow month truncation to change the result. (Jan 31st + 1 month) + 1 month will not actually give the same result as Jan 31st + 2 months. You want to add to the interval, then apply to the original date again, not to some accumulated date.
830 
831 	History:
832 		Added December 13, 2025
833 +/
834 struct PackedInterval {
835 	private ulong packedData;
836 
837 	this(int months, int days = 0, int milliseconds = 0) {
838 		this.months = months;
839 		this.days = days;
840 		this.milliseconds = milliseconds;
841 	}
842 
843 	/++
844 		Getters and setters for the components
845 	+/
846 	short months() const {
847 		return cast(short)((packedData >> 48) & 0xffff);
848 	}
849 
850 	/// ditto
851 	short days() const {
852 		return cast(short)((packedData >> 32) & 0xffff);
853 	}
854 
855 	/// ditto
856 	int milliseconds() const {
857 		return cast(int)(packedData & 0xffff_ffff);
858 	}
859 
860 	/// ditto
861 	void months(int v) {
862 		short d = cast(short) v;
863 		ulong s = d;
864 		packedData &= ~(0xffffUL << 48);
865 		packedData |= s << 48;
866 	}
867 
868 	/// ditto
869 	void days(int v) {
870 		short d = cast(short) v;
871 		ulong s = d;
872 		packedData &= ~(0xffffUL << 32);
873 		packedData |= s << 32;
874 	}
875 
876 	/// ditto
877 	void milliseconds(int v) {
878 		packedData &= 0xffffffff_00000000UL;
879 		packedData |= cast(ulong) v;
880 	}
881 
882 	PackedInterval opBinary(string op : "*")(int iterations) const {
883 		return PackedInterval(this.months * iterations, this.days * iterations, this.milliseconds * iterations);
884 	}
885 }
886 
887 unittest {
888 	PackedInterval pi = PackedInterval(1);
889 	assert(pi.months == 1);
890 	assert(pi.days == 0);
891 	assert(pi.milliseconds == 0);
892 }
893 
894 /++
895 	Basically a Phobos SysTime but standing alone as a simple 64 bit integer (but wrapped) for compatibility with LimitedVariant.
896 +/
897 struct SimplifiedUtcTimestamp {
898 	long timestamp;
899 
900 	this(long hnsecTimestamp) {
901 		this.timestamp = hnsecTimestamp;
902 	}
903 
904 	// this(PackedDateTime pdt)
905 
906 	string toString() const {
907 		import core.stdc.time;
908 		char[128] buffer;
909 		auto ut = toUnixTime();
910 		tm* t = gmtime(&ut);
911 		if(t is null)
912 			return "null time";
913 
914 		return buffer[0 .. strftime(buffer.ptr, buffer.length, "%Y-%m-%dT%H:%M:%SZ", t)].idup;
915 	}
916 
917 	version(Windows)
918 		alias time_t = int;
919 
920 	static SimplifiedUtcTimestamp fromUnixTime(time_t t) {
921 		return SimplifiedUtcTimestamp(621_355_968_000_000_000L + t * 1_000_000_000L / 100);
922 	}
923 
924 	/++
925 		History:
926 			Added November 22, 2025
927 	+/
928 	static SimplifiedUtcTimestamp now() {
929 		import core.stdc.time;
930 		return SimplifiedUtcTimestamp.fromUnixTime(time(null));
931 	}
932 
933 	time_t toUnixTime() const {
934 		return cast(time_t) ((timestamp - 621_355_968_000_000_000L) / 1_000_000_0); // hnsec = 7 digits
935 	}
936 
937 	long stdTime() const {
938 		return timestamp;
939 	}
940 
941 	SimplifiedUtcTimestamp opBinary(string op : "+")(Duration d) const {
942 		return SimplifiedUtcTimestamp(this.timestamp + d.total!"hnsecs");
943 	}
944 }
945 
946 unittest {
947 	SimplifiedUtcTimestamp sut = SimplifiedUtcTimestamp.fromUnixTime(86_400);
948 	assert(sut.toString() == "1970-01-02T00:00:00Z");
949 }
950 
951 /++
952 	A little builder pattern helper that is meant for use by other library code.
953 
954 	History:
955 		Added October 31, 2025
956 +/
957 struct AdHocBuiltStruct(string tag, string[] names = [], T...) {
958 	static assert(names.length == T.length);
959 
960 	T values;
961 
962 	auto opDispatch(string name, Arg)(Arg value) {
963 		return AdHocBuiltStruct!(tag, names ~ name, T, Arg)(values, value);
964 	}
965 }
966 
967 unittest {
968 	AdHocBuiltStruct!"tag"()
969 		.id(5)
970 		.name("five")
971 	;
972 }
973 
974 /++
975 	Represents a generic raw element to be embedded in an interpolated sequence.
976 
977 	Use with caution, its exact meaning is dependent on the specific function being called, but it generally is meant to disable encoding protections the function normally provides.
978 
979 	History:
980 		Added October 31, 2025
981 +/
982 struct iraw {
983 	string s;
984 
985 	@system this(string s) {
986 		this.s = s;
987 	}
988 }
989 
990 /++
991 	Counts the number of bits set to `1` in a value, using intrinsics when available.
992 
993 	History:
994 		Added December 15, 2025
995 +/
996 int countOfBitsSet(ulong v) {
997 	version(LDC) {
998 		import ldc.intrinsics;
999 		return cast(int) llvm_ctpop(v);
1000 	} else {
1001 		// kerninghan's algorithm
1002 		int count = 0;
1003 		while(v) {
1004 			v &= v - 1;
1005 			count++;
1006 		}
1007 		return count;
1008 	}
1009 }
1010 
1011 unittest {
1012 	assert(countOfBitsSet(0) == 0);
1013 	assert(countOfBitsSet(ulong.max) == 64);
1014 	assert(countOfBitsSet(0x0f0f) == 8);
1015 	assert(countOfBitsSet(0x0f0f2) == 9);
1016 }
1017 
1018 /++
1019 	A limited variant to hold just a few types. It is made for the use of packing a small amount of extra data into error messages and some transit across virtual function boundaries.
1020 
1021 	Note that empty strings and null values are indistinguishable unless you explicitly slice the end of some other existing string!
1022 +/
1023 /+
1024 	ALL OF THESE ARE SUBJECT TO CHANGE
1025 
1026 	* if length and ptr are both 0, it is null
1027 	* if ptr == 1, length is an integer
1028 	* if ptr == 2, length is an unsigned integer (suggest printing in hex)
1029 	* if ptr == 3, length is a combination of flags (suggest printing in binary)
1030 	* if ptr == 4, length is a unix permission thing (suggest printing in octal)
1031 	* if ptr == 5, length is a double float
1032 	* if ptr == 6, length is an Object ref (reinterpret casted to void*)
1033 
1034 	* if ptr == 7, length is a ticks count (from MonoTime)
1035 	* if ptr == 8, length is a utc timestamp (hnsecs)
1036 	* if ptr == 9, length is a duration (signed hnsecs)
1037 	* if ptr == 10, length is a date or date time (bit packed, see flags in data to determine if it is a Date, Time, or DateTime)
1038 	* if ptr == 11, length is a decimal
1039 
1040 	* if ptr == 12, length is a bool (redundant to int?)
1041 	13, 14 reserved. maybe char?
1042 
1043 	* if ptr == 15, length must be 0. this holds an empty, non-null, SSO string.
1044 	* if ptr >= 16 && < 24, length is reinterpret-casted a small string of length of (ptr & 0x7) + 1
1045 
1046 	* if length == size_t.max, ptr is interpreted as a stringz
1047 	* if ptr >= 1024, it is a non-null D string or byte array. It is a string if the length high bit is clear, a byte array if it is set. the length is what is left after you mask that out.
1048 
1049 	All other ptr values are reserved for future expansion.
1050 
1051 	It basically can store:
1052 		null
1053 			type details = must be 0
1054 		int (actually long)
1055 			type details = formatting hints
1056 		float (actually double)
1057 			type details = formatting hints
1058 		dchar (actually enum - upper half is the type tag, lower half is the member tag)
1059 			type details = ???
1060 		decimal
1061 			type details = precision specifier
1062 		object
1063 			type details = ???
1064 		timestamp
1065 			type details: ticks, utc timestamp, relative duration
1066 
1067 		sso
1068 		stringz
1069 
1070 		or it is bytes or a string; a normal D array (just bytes has a high bit set on length).
1071 
1072 	But there are subtypes of some of those; ints can just have formatting hints attached.
1073 		Could reserve 0-7 as low level type flag (null, int, float, pointer, object)
1074 		15-24 still can be the sso thing
1075 
1076 		We have 10 bits really.
1077 
1078 		00000 00000
1079 		????? OOLLL
1080 
1081 		The ????? are type details bits.
1082 
1083 	64 bits decmial to 4 points of precision needs... 14 bits for the small part (so max of 4 digits)? so 50 bits for the big part (max of about 1 quadrillion)
1084 		...actually it can just be a dollars * 10000 + cents * 100.
1085 
1086 +/
1087 struct LimitedVariant {
1088 
1089 	/++
1090 
1091 	+/
1092 	enum Contains {
1093 		null_,
1094 		intDecimal,
1095 		intHex,
1096 		intBinary,
1097 		intOctal,
1098 		double_,
1099 		object,
1100 
1101 		monoTime,
1102 		utcTimestamp,
1103 		duration,
1104 		dateTime,
1105 		decimal,
1106 
1107 		// FIXME interval like postgres? e.g. 30 days, 2 months. distinct from Duration, which is a difference of monoTimes or utcTimestamps, interval is more like a difference of PackedDateTime.
1108 		// FIXME boolean? char? specializations of float for various precisions...
1109 
1110 		// could do enums by way of a pointer but kinda iffy
1111 
1112 		// maybe some kind of prefixed string too for stuff like xml and json or enums etc.
1113 
1114 		// fyi can also use stringzs or length-prefixed string pointers
1115 		emptySso,
1116 		stringSso,
1117 		stringz,
1118 		string,
1119 		bytes,
1120 
1121 		invalid,
1122 	}
1123 
1124 	/++
1125 		Each datum stored in the LimitedVariant has a tag associated with it.
1126 
1127 		Each tag belongs to one or more data families.
1128 	+/
1129 	Contains contains() const {
1130 		auto tag = cast(size_t) ptr;
1131 		if(ptr is null && length is null)
1132 			return Contains.null_;
1133 		else switch(tag) {
1134 			case 1: return Contains.intDecimal;
1135 			case 2: return Contains.intHex;
1136 			case 3: return Contains.intBinary;
1137 			case 4: return Contains.intOctal;
1138 			case 5: return Contains.double_;
1139 			case 6: return Contains.object;
1140 
1141 			case 7: return Contains.monoTime;
1142 			case 8: return Contains.utcTimestamp;
1143 			case 9: return Contains.duration;
1144 			case 10: return Contains.dateTime;
1145 			case 11: return Contains.decimal;
1146 
1147 			case 15: return length is null ? Contains.emptySso : Contains.invalid;
1148 			default:
1149 				if(tag >= 16 && tag < 24) {
1150 					return Contains.stringSso;
1151 				} else if(tag >= 1024) {
1152 					if(cast(size_t) length == size_t.max)
1153 						return Contains.stringz;
1154 					else
1155 						return isHighBitSet ? Contains.bytes : Contains..string;
1156 				} else {
1157 					return isHighBitSet ? Contains.bytes : Contains.invalid;
1158 				}
1159 		}
1160 	}
1161 
1162 	/// ditto
1163 	bool containsNull() const {
1164 		return contains() == Contains.null_;
1165 	}
1166 
1167 	/// ditto
1168 	bool containsDecimal() const {
1169 		return contains() == Contains.decimal;
1170 	}
1171 
1172 	/// ditto
1173 	bool containsInt() const {
1174 		with(Contains)
1175 		switch(contains) {
1176 			case intDecimal, intHex, intBinary, intOctal:
1177 				return true;
1178 			default:
1179 				return false;
1180 		}
1181 	}
1182 
1183 	// all specializations of int...
1184 
1185 	/// ditto
1186 	bool containsMonoTime() const {
1187 		return contains() == Contains.monoTime;
1188 	}
1189 	/// ditto
1190 	bool containsUtcTimestamp() const {
1191 		return contains() == Contains.utcTimestamp;
1192 	}
1193 	/// ditto
1194 	bool containsDuration() const {
1195 		return contains() == Contains.duration;
1196 	}
1197 	/// ditto
1198 	bool containsDateTime() const {
1199 		return contains() == Contains.dateTime;
1200 	}
1201 
1202 	// done int specializations
1203 
1204 	/// ditto
1205 	bool containsString() const {
1206 		with(Contains)
1207 		switch(contains) {
1208 			case null_, emptySso, stringSso, string:
1209 			case stringz:
1210 				return true;
1211 			default:
1212 				return false;
1213 		}
1214 	}
1215 
1216 	/// ditto
1217 	bool containsDouble() const {
1218 		with(Contains)
1219 		switch(contains) {
1220 			case double_:
1221 				return true;
1222 			default:
1223 				return false;
1224 		}
1225 	}
1226 
1227 	/// ditto
1228 	bool containsBytes() const {
1229 		with(Contains)
1230 		switch(contains) {
1231 			case bytes, null_:
1232 				return true;
1233 			default:
1234 				return false;
1235 		}
1236 	}
1237 
1238 	private const(void)* length;
1239 	private const(ubyte)* ptr;
1240 
1241 	private void Throw() const {
1242 		throw ArsdException!"LimitedVariant"(cast(size_t) length, cast(size_t) ptr);
1243 	}
1244 
1245 	private bool isHighBitSet() const {
1246 		return (cast(size_t) length >> (size_t.sizeof * 8 - 1) & 0x1) != 0;
1247 	}
1248 
1249 	/++
1250 		getString gets a reference to the string stored internally, which may be a temporary. See [toString] to get a normal string representation or whatever is inside.
1251 
1252 	+/
1253 	const(char)[] getString() const return {
1254 		with(Contains)
1255 		switch(contains()) {
1256 			case null_:
1257 				return null;
1258 			case emptySso:
1259 				return (cast(const(char)*) ptr)[0 .. 0]; // zero length, non-null
1260 			case stringSso:
1261 				auto len = ((cast(size_t) ptr) & 0x7) + 1;
1262 				return (cast(char*) &length)[0 .. len];
1263 			case string:
1264 				return (cast(const(char)*) ptr)[0 .. cast(size_t) length];
1265 			case stringz:
1266 				return arsd.core.stringz(cast(char*) ptr).borrow;
1267 			default:
1268 				Throw(); assert(0);
1269 		}
1270 	}
1271 
1272 	/// ditto
1273 	long getInt() const {
1274 		if(containsInt)
1275 			return cast(long) length;
1276 		else
1277 			Throw();
1278 		assert(0);
1279 	}
1280 
1281 	/// ditto
1282 	double getDouble() const {
1283 		if(containsDouble) {
1284 			floathack hack;
1285 			hack.e = cast(void*) length; // casting away const
1286 			return hack.d;
1287 		} else
1288 			Throw();
1289 		assert(0);
1290 	}
1291 
1292 	/// ditto
1293 	const(ubyte)[] getBytes() const {
1294 		with(Contains)
1295 		switch(contains()) {
1296 			case null_:
1297 				return null;
1298 			case bytes:
1299 				return ptr[0 .. (cast(size_t) length) & ((1UL << (size_t.sizeof * 8 - 1)) - 1)];
1300 			default:
1301 				Throw(); assert(0);
1302 		}
1303 	}
1304 
1305 	/// ditto
1306 	Object getObject() const {
1307 		with(Contains)
1308 		switch(contains()) {
1309 			case null_:
1310 				return null;
1311 			case object:
1312 				return cast(Object) length; // FIXME const correctness sigh
1313 			default:
1314 				Throw(); assert(0);
1315 		}
1316 	}
1317 
1318 	/// ditto
1319 	MonoTime getMonoTime() const {
1320 		if(containsMonoTime) {
1321 			MonoTime time;
1322 			__traits(getMember, time, "_ticks") = cast(long) length;
1323 			return time;
1324 		} else
1325 			Throw();
1326 		assert(0);
1327 	}
1328 	/// ditto
1329 	SimplifiedUtcTimestamp getUtcTimestamp() const {
1330 		if(containsUtcTimestamp)
1331 			return SimplifiedUtcTimestamp(cast(long) length);
1332 		else
1333 			Throw();
1334 		assert(0);
1335 	}
1336 	/// ditto
1337 	Duration getDuration() const {
1338 		if(containsDuration)
1339 			return hnsecs(cast(long) length);
1340 		else
1341 			Throw();
1342 		assert(0);
1343 	}
1344 	/// ditto
1345 	PackedDateTime getDateTime() const {
1346 		if(containsDateTime)
1347 			return PackedDateTime(cast(long) length);
1348 		else
1349 			Throw();
1350 		assert(0);
1351 	}
1352 
1353 	/// ditto
1354 	DynamicDecimal getDecimal() const {
1355 		if(containsDecimal)
1356 			return DynamicDecimal(cast(long) length);
1357 		else
1358 			Throw();
1359 		assert(0);
1360 	}
1361 
1362 
1363 	/++
1364 
1365 	+/
1366 	string toString() const {
1367 
1368 		string intHelper(string prefix, int radix) {
1369 			char[128] buffer;
1370 			buffer[0 .. prefix.length] = prefix[];
1371 			char[] toUse = buffer[prefix.length .. $];
1372 
1373 			auto got = intToString(getInt(), toUse[], IntToStringArgs().withRadix(radix));
1374 
1375 			return buffer[0 .. prefix.length + got.length].idup;
1376 		}
1377 
1378 		with(Contains)
1379 		final switch(contains()) {
1380 			case null_:
1381 				return "<null>";
1382 			case intDecimal:
1383 				return intHelper("", 10);
1384 			case intHex:
1385 				return intHelper("0x", 16);
1386 			case intBinary:
1387 				return intHelper("0b", 2);
1388 			case intOctal:
1389 				return intHelper("0o", 8);
1390 			case emptySso, stringSso, string, stringz:
1391 				return getString().idup;
1392 			case bytes:
1393 				auto b = getBytes();
1394 
1395 				return "<bytes>"; // FIXME
1396 			case object:
1397 				auto o = getObject();
1398 				return o is null ? "null" : o.toString();
1399 			case monoTime:
1400 				return getMonoTime.toString();
1401 			case utcTimestamp:
1402 				return getUtcTimestamp().toString();
1403 			case duration:
1404 				return getDuration().toString();
1405 			case dateTime:
1406 				return getDateTime().toString();
1407 			case decimal:
1408 				return getDecimal().toString();
1409 			case double_:
1410 				auto d = getDouble();
1411 
1412 				import core.stdc.stdio;
1413 				char[64] buffer;
1414 				auto count = snprintf(buffer.ptr, buffer.length, "%.17lf", d);
1415 				return buffer[0 .. count].idup;
1416 			case invalid:
1417 				return "<invalid>";
1418 		}
1419 	}
1420 
1421 	/++
1422 		Note for integral types that are not `int` and `long` (for example, `short` or `ubyte`), you might want to explicitly convert them to `int`.
1423 	+/
1424 	this(string s) {
1425 		ptr = cast(const(ubyte)*) s.ptr;
1426 		length = cast(void*) s.length;
1427 	}
1428 
1429 	/// ditto
1430 	this(const(char)* stringz) {
1431 		if(stringz !is null) {
1432 			ptr = cast(const(ubyte)*) stringz;
1433 			length = cast(void*) size_t.max;
1434 		} else {
1435 			ptr = null;
1436 			length = null;
1437 		}
1438 	}
1439 
1440 	/// ditto
1441 	this(const(ubyte)[] b) {
1442 		ptr = cast(const(ubyte)*) b.ptr;
1443 		length = cast(void*) (b.length | (1UL << (size_t.sizeof * 8 - 1)));
1444 	}
1445 
1446 	/// ditto
1447 	this(long l, int base = 10) {
1448 		int tag;
1449 		switch(base) {
1450 			case 10: tag = 1; break;
1451 			case 16: tag = 2; break;
1452 			case  2: tag = 3; break;
1453 			case  8: tag = 4; break;
1454 			default: assert(0, "You passed an invalid base to LimitedVariant");
1455 		}
1456 		ptr = cast(ubyte*) tag;
1457 		length = cast(void*) l;
1458 	}
1459 
1460 	/// ditto
1461 	this(int i, int base = 10) {
1462 		this(cast(long) i, base);
1463 	}
1464 
1465 	/// ditto
1466 	this(bool i) {
1467 		// FIXME?
1468 		this(cast(long) i);
1469 	}
1470 
1471 	/// ditto
1472 	this(double d) {
1473 		// the reinterpret cast hack crashes dmd! omg
1474 		ptr = cast(ubyte*) 5;
1475 
1476 		floathack h;
1477 		h.d = d;
1478 
1479 		this.length = h.e;
1480 	}
1481 
1482 	/// ditto
1483 	this(Object o) {
1484 		this.ptr = cast(ubyte*) 6;
1485 		this.length = cast(void*) o;
1486 	}
1487 
1488 	/// ditto
1489 	this(MonoTime a) {
1490 		this.ptr = cast(ubyte*) 7;
1491 		this.length = cast(void*) a.ticks;
1492 	}
1493 
1494 	/// ditto
1495 	this(SimplifiedUtcTimestamp a) {
1496 		this.ptr = cast(ubyte*) 8;
1497 		this.length = cast(void*) a.timestamp;
1498 	}
1499 
1500 	/// ditto
1501 	this(Duration a) {
1502 		this.ptr = cast(ubyte*) 9;
1503 		this.length = cast(void*) a.total!"hnsecs";
1504 	}
1505 
1506 	/// ditto
1507 	this(PackedDateTime a) {
1508 		this.ptr = cast(ubyte*) 10;
1509 		this.length = cast(void*) a.packedData;
1510 	}
1511 
1512 	/// ditto
1513 	this(DynamicDecimal a) {
1514 		this.ptr = cast(ubyte*) 11;
1515 		this.length = cast(void*) a.storage;
1516 	}
1517 }
1518 
1519 unittest {
1520 	LimitedVariant v = LimitedVariant("foo");
1521 	assert(v.containsString());
1522 	assert(!v.containsInt());
1523 	assert(v.getString() == "foo");
1524 
1525 	LimitedVariant v2 = LimitedVariant(4);
1526 	assert(v2.containsInt());
1527 	assert(!v2.containsString());
1528 	assert(v2.getInt() == 4);
1529 
1530 	LimitedVariant v3 = LimitedVariant(cast(ubyte[]) [1, 2, 3]);
1531 	assert(v3.containsBytes());
1532 	assert(!v3.containsString());
1533 	assert(v3.getBytes() == [1, 2, 3]);
1534 }
1535 
1536 private union floathack {
1537 	// in 32 bit we'll use float instead since it at least fits in the void*
1538 	static if(double.sizeof == (void*).sizeof) {
1539 		double d;
1540 	} else {
1541 		float d;
1542 	}
1543 	void* e;
1544 }
1545 
1546 /+
1547 	64 bit signed goes up to 9.22x10^18
1548 
1549 	3 bit precision = 0-7
1550 	60 bits remain for the value = 1.15x10^18.
1551 
1552 	so you can use up to 10 digits decimal 7 digits.
1553 
1554 	9,999,999,999.9999999
1555 
1556 	math between decimals must always have the same precision on both sides.
1557 
1558 	decimal and 32 bit int is always allowed assuming the int is a whole number.
1559 
1560 	FIXME add this to LimitedVariant
1561 +/
1562 /++
1563 	A DynamicDecimal is a fixed-point object whose precision is dynamically typed.
1564 
1565 
1566 	It packs everything into a 64 bit value. It uses one bit for sign, three bits
1567 	for precision, then the rest of them for the value. This means the precision
1568 	(that is, the number of digits after the decimal) can be from 0 to 7, and there
1569 	can be a total of 18 digits.
1570 
1571 	Numbers can be added and subtracted only if they have matching precision. They can
1572 	be multiplied and divided only by whole numbers.
1573 
1574 	History:
1575 		Added December 12, 2025.
1576 +/
1577 struct DynamicDecimal {
1578 	private ulong storage;
1579 
1580 	private this(ulong storage) {
1581 		this.storage = storage;
1582 	}
1583 
1584 	this(long value, int precision) {
1585 		assert(precision >= 0 && precision <= 7);
1586 		bool isNeg = value < 0;
1587 		if(isNeg)
1588 			value = -value;
1589 		assert((value & 0xf000_0000_0000_0000) == 0);
1590 
1591 		storage =
1592 			(isNeg ? 0x8000_0000_0000_0000 : 0)
1593 			|
1594 			(cast(ulong) precision << 60)
1595 			|
1596 			(value)
1597 		;
1598 	}
1599 
1600 	private bool isNegative() {
1601 		return (storage >> 63) ? true : false;
1602 	}
1603 
1604 	/++
1605 	+/
1606 	int precision() {
1607 		return (storage >> 60) & 7;
1608 	}
1609 
1610 	/++
1611 	+/
1612 	long value() {
1613 		long omg = storage & 0x0fff_ffff_ffff_ffff;
1614 		if(isNegative)
1615 			omg = -omg;
1616 		return omg;
1617 	}
1618 
1619 	/++
1620 		Some basic arithmetic operators are defined on this: +, -, *, and /, but only between
1621 		numbers of the same precision. Note that division always returns the quotient and remainder
1622 		together in one return and any overflowing operations will also throw.
1623 	+/
1624 	typeof(this) opBinary(string op)(typeof(this) rhs) if(op == "+" || op == "-") {
1625 		assert(this.precision == rhs.precision);
1626 		return typeof(this)(mixin("this.value" ~ op ~ "rhs.value"), this.precision);
1627 	}
1628 
1629 	/// ditto
1630 	typeof(this) opBinary(string op)(int rhs) if(op == "*") {
1631 		// what if we overflow on the multiplication? FIXME
1632 		return typeof(this)(this.value * rhs, this.precision);
1633 	}
1634 
1635 	/// ditto
1636 	static struct DivisionResult {
1637 		DynamicDecimal quotient;
1638 		DynamicDecimal remainder;
1639 	}
1640 
1641 	/// ditto
1642 	DivisionResult opBinary(string op)(int rhs) if(op == "/") {
1643 		return DivisionResult(typeof(this)(this.value / rhs, this.precision), typeof(this)(this.value % rhs, this.precision));
1644 	}
1645 
1646 	/// ditto
1647 	typeof(this) opUnary(string op : "-")() {
1648 		return typeof(this)(-this.value, this.precision);
1649 	}
1650 
1651 	/// ditto
1652 	long opCmp(typeof(this) rhs) {
1653 		assert(this.precision == rhs.precision);
1654 		return this.value - rhs.value;
1655 	}
1656 
1657 	/++
1658 		Converts to a floating point type. There's potentially a loss of precision here.
1659 	+/
1660 	double toFloatingPoint() {
1661 		long divisor = 1;
1662 		foreach(i; 0 .. this.precision)
1663 			divisor *= 10;
1664 		return cast(double) this.value / divisor;
1665 	}
1666 
1667 	/++
1668 	+/
1669 	string toString(int minimumNumberOfDigitsLeftOfDecimal = 1) @system {
1670 		char[64] buffer = void;
1671 		// FIXME: what about a group separator arg?
1672 		IntToStringArgs args = IntToStringArgs().
1673 			withPadding(minimumNumberOfDigitsLeftOfDecimal + this.precision);
1674 		auto got = intToString(this.value, buffer[], args);
1675 		assert(got.length >= this.precision);
1676 		int digitsLeftOfDecimal = cast(int) got.length - this.precision;
1677 		auto toShift = buffer[got.length - this.precision .. got.length];
1678 		import core.stdc.string;
1679 		memmove(toShift.ptr + 1, toShift.ptr, toShift.length);
1680 		toShift[0] = '.';
1681 		return buffer[0 .. got.length + 1].idup;
1682 	}
1683 }
1684 
1685 unittest {
1686 	DynamicDecimal a = DynamicDecimal(100, 2);
1687 	auto res = a / 3;
1688 	assert(res.quotient.value == 33);
1689 	assert(res.remainder.value == 1);
1690 	res = a / 2;
1691 	assert(res.quotient.value == 50);
1692 	assert(res.remainder.value == 0);
1693 
1694 	assert(res.quotient.toFloatingPoint == 0.50);
1695 	assert(res.quotient.toString() == "0.50");
1696 
1697 	assert((a * 2).value == 200);
1698 
1699 	DynamicDecimal b = DynamicDecimal(1, 4);
1700 	assert(b.toFloatingPoint() == 0.0001);
1701 	assert(b.toString() == "0.0001");
1702 
1703 	assert(a > (a / 2).quotient);
1704 }
1705 
1706 /++
1707 	This is a dummy type to indicate the end of normal arguments and the beginning of the file/line inferred args.  It is meant to ensure you don't accidentally send a string that is interpreted as a filename when it was meant to be a normal argument to the function and trigger the wrong overload.
1708 +/
1709 struct ArgSentinel {}
1710 
1711 /++
1712 	A trivial wrapper around C's malloc that creates a D slice. It multiples n by T.sizeof and returns the slice of the pointer from 0 to n.
1713 
1714 	Please note that the ptr might be null - it is your responsibility to check that, same as normal malloc. Check `ret is null` specifically, since `ret.length` will always be `n`, even if the `malloc` failed.
1715 
1716 	Remember to `free` the returned pointer with `core.stdc.stdlib.free(ret.ptr);`
1717 
1718 	$(TIP
1719 		I strongly recommend you simply use the normal garbage collector unless you have a very specific reason not to.
1720 	)
1721 
1722 	See_Also:
1723 		[mallocedStringz]
1724 +/
1725 T[] mallocSlice(T)(size_t n) {
1726 	import c = core.stdc.stdlib;
1727 
1728 	return (cast(T*) c.malloc(n * T.sizeof))[0 .. n];
1729 }
1730 
1731 /++
1732 	Uses C's malloc to allocate a copy of `original` with an attached zero terminator. It may return a slice with a `null` pointer (but non-zero length!) if `malloc` fails and you are responsible for freeing the returned pointer with `core.stdc.stdlib.free(ret.ptr)`.
1733 
1734 	$(TIP
1735 		I strongly recommend you use [CharzBuffer] or Phobos' [std.string.toStringz] instead unless there's a special reason not to.
1736 	)
1737 
1738 	See_Also:
1739 		[CharzBuffer] for a generally better alternative. You should only use `mallocedStringz` where `CharzBuffer` cannot be used (e.g. when druntime is not usable or you have no stack space for the temporary buffer).
1740 
1741 		[mallocSlice] is the function this function calls, so the notes in its documentation applies here too.
1742 +/
1743 char[] mallocedStringz(in char[] original) {
1744 	auto slice = mallocSlice!char(original.length + 1);
1745 	if(slice is null)
1746 		return null;
1747 	slice[0 .. original.length] = original[];
1748 	slice[original.length] = 0;
1749 	return slice;
1750 }
1751 
1752 /++
1753 	Basically a `scope class` you can return from a function or embed in another aggregate.
1754 +/
1755 struct OwnedClass(Class) {
1756 	ubyte[__traits(classInstanceSize, Class)] rawData;
1757 
1758 	static OwnedClass!Class defaultConstructed() {
1759 		OwnedClass!Class i = OwnedClass!Class.init;
1760 		i.initializeRawData();
1761 		return i;
1762 	}
1763 
1764 	private void initializeRawData() @trusted {
1765 		if(!this)
1766 			rawData[] = cast(ubyte[]) typeid(Class).initializer[];
1767 	}
1768 
1769 	this(T...)(T t) {
1770 		initializeRawData();
1771 		rawInstance.__ctor(t);
1772 	}
1773 
1774 	bool opCast(T : bool)() @trusted {
1775 		return !(*(cast(void**) rawData.ptr) is null);
1776 	}
1777 
1778 	@disable this();
1779 	@disable this(this);
1780 
1781 	Class rawInstance() return @trusted {
1782 		if(!this)
1783 			throw new Exception("null");
1784 		return cast(Class) rawData.ptr;
1785 	}
1786 
1787 	alias rawInstance this;
1788 
1789 	~this() @trusted {
1790 		if(this)
1791 			.destroy(rawInstance());
1792 	}
1793 }
1794 
1795 // might move RecyclableMemory here
1796 
1797 version(Posix)
1798 package(arsd) void makeNonBlocking(int fd) {
1799 	import core.sys.posix.fcntl;
1800 	auto flags = fcntl(fd, F_GETFL, 0);
1801 	if(flags == -1)
1802 		throw new ErrnoApiException("fcntl get", errno);
1803 	flags |= O_NONBLOCK;
1804 	auto s = fcntl(fd, F_SETFL, flags);
1805 	if(s == -1)
1806 		throw new ErrnoApiException("fcntl set", errno);
1807 }
1808 
1809 version(Posix)
1810 package(arsd) void setCloExec(int fd) {
1811 	import core.sys.posix.fcntl;
1812 	auto flags = fcntl(fd, F_GETFD, 0);
1813 	if(flags == -1)
1814 		throw new ErrnoApiException("fcntl get", errno);
1815 	flags |= FD_CLOEXEC;
1816 	auto s = fcntl(fd, F_SETFD, flags);
1817 	if(s == -1)
1818 		throw new ErrnoApiException("fcntl set", errno);
1819 }
1820 
1821 
1822 /++
1823 	A helper object for temporarily constructing a string appropriate for the Windows API from a D UTF-8 string.
1824 
1825 
1826 	It will use a small internal static buffer is possible, and allocate a new buffer if the string is too big.
1827 
1828 	History:
1829 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
1830 +/
1831 version(Windows)
1832 struct WCharzBuffer {
1833 	private wchar[] buffer;
1834 	private wchar[128] staticBuffer = void;
1835 
1836 	/// Length of the string, excluding the zero terminator.
1837 	size_t length() {
1838 		return buffer.length;
1839 	}
1840 
1841 	// Returns the pointer to the internal buffer. You must assume its lifetime is less than that of the WCharzBuffer. It is zero-terminated.
1842 	wchar* ptr() {
1843 		return buffer.ptr;
1844 	}
1845 
1846 	/// Returns the slice of the internal buffer, excluding the zero terminator (though there is one present right off the end of the slice). You must assume its lifetime is less than that of the WCharzBuffer.
1847 	wchar[] slice() {
1848 		return buffer;
1849 	}
1850 
1851 	/// Copies it into a static array of wchars
1852 	void copyInto(R)(ref R r) {
1853 		static if(is(R == wchar[N], size_t N)) {
1854 			r[0 .. this.length] = slice[];
1855 			r[this.length] = 0;
1856 		} else static assert(0, "can only copy into wchar[n], not " ~ R.stringof);
1857 	}
1858 
1859 	/++
1860 		conversionFlags = [WindowsStringConversionFlags]
1861 	+/
1862 	this(in char[] data, int conversionFlags = 0) {
1863 		conversionFlags |= WindowsStringConversionFlags.zeroTerminate; // this ALWAYS zero terminates cuz of its name
1864 		auto sz = sizeOfConvertedWstring(data, conversionFlags);
1865 		if(sz > staticBuffer.length)
1866 			buffer = new wchar[](sz);
1867 		else
1868 			buffer = staticBuffer[];
1869 
1870 		buffer = makeWindowsString(data, buffer, conversionFlags);
1871 	}
1872 }
1873 
1874 /++
1875 	Alternative for toStringz
1876 
1877 	History:
1878 		Added March 18, 2023 (dub v11.0)
1879 +/
1880 struct CharzBuffer {
1881 	private char[] buffer;
1882 	private char[128] staticBuffer = void;
1883 
1884 	/// Length of the string, excluding the zero terminator.
1885 	size_t length() {
1886 		assert(buffer.length > 0);
1887 		return buffer.length - 1;
1888 	}
1889 
1890 	// Returns the pointer to the internal buffer. You must assume its lifetime is less than that of the CharzBuffer. It is zero-terminated.
1891 	char* ptr() {
1892 		return buffer.ptr;
1893 	}
1894 
1895 	/// Returns the slice of the internal buffer, excluding the zero terminator (though there is one present right off the end of the slice). You must assume its lifetime is less than that of the CharzBuffer.
1896 	char[] slice() {
1897 		assert(buffer.length > 0);
1898 		return buffer[0 .. $-1];
1899 	}
1900 
1901 	/// Copies it into a static array of chars
1902 	void copyInto(R)(ref R r) {
1903 		static if(is(R == char[N], size_t N)) {
1904 			r[0 .. this.length] = slice[];
1905 			r[this.length] = 0;
1906 		} else static assert(0, "can only copy into char[n], not " ~ R.stringof);
1907 	}
1908 
1909 	@disable this();
1910 	@disable this(this);
1911 
1912 	/++
1913 		Copies `data` into the CharzBuffer, allocating a new one if needed, and zero-terminates it.
1914 	+/
1915 	this(in char[] data) {
1916 		if(data.length + 1 > staticBuffer.length)
1917 			buffer = new char[](data.length + 1);
1918 		else
1919 			buffer = staticBuffer[];
1920 
1921 		buffer[0 .. data.length] = data[];
1922 		buffer[data.length] = 0;
1923 		buffer = buffer[0 .. data.length + 1];
1924 	}
1925 }
1926 
1927 /++
1928 	Given the string `str`, converts it to a string compatible with the Windows API and puts the result in `buffer`, returning the slice of `buffer` actually used. `buffer` must be at least [sizeOfConvertedWstring] elements long.
1929 
1930 	History:
1931 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
1932 +/
1933 version(Windows)
1934 wchar[] makeWindowsString(in char[] str, wchar[] buffer, int conversionFlags = WindowsStringConversionFlags.zeroTerminate) {
1935 	if(str.length == 0)
1936 		return null;
1937 
1938 	int pos = 0;
1939 	dchar last;
1940 	foreach(dchar c; str) {
1941 		if(c <= 0xFFFF) {
1942 			if((conversionFlags & WindowsStringConversionFlags.convertNewLines) && c == 10 && last != 13)
1943 				buffer[pos++] = 13;
1944 			buffer[pos++] = cast(wchar) c;
1945 		} else if(c <= 0x10FFFF) {
1946 			buffer[pos++] = cast(wchar)((((c - 0x10000) >> 10) & 0x3FF) + 0xD800);
1947 			buffer[pos++] = cast(wchar)(((c - 0x10000) & 0x3FF) + 0xDC00);
1948 		}
1949 
1950 		last = c;
1951 	}
1952 
1953 	if(conversionFlags & WindowsStringConversionFlags.zeroTerminate) {
1954 		buffer[pos] = 0;
1955 	}
1956 
1957 	return buffer[0 .. pos];
1958 }
1959 
1960 /++
1961 	Converts the Windows API string `str` to a D UTF-8 string, storing it in `buffer`. Returns the slice of `buffer` actually used.
1962 
1963 	History:
1964 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
1965 +/
1966 version(Windows)
1967 char[] makeUtf8StringFromWindowsString(in wchar[] str, char[] buffer) {
1968 	if(str.length == 0)
1969 		return null;
1970 
1971 	auto got = WideCharToMultiByte(CP_UTF8, 0, str.ptr, cast(int) str.length, buffer.ptr, cast(int) buffer.length, null, null);
1972 	if(got == 0) {
1973 		if(GetLastError() == ERROR_INSUFFICIENT_BUFFER)
1974 			throw new object.Exception("not enough buffer");
1975 		else
1976 			throw new object.Exception("conversion"); // FIXME: GetLastError
1977 	}
1978 	return buffer[0 .. got];
1979 }
1980 
1981 /++
1982 	Converts the Windows API string `str` to a newly-allocated D UTF-8 string.
1983 
1984 	History:
1985 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
1986 +/
1987 version(Windows)
1988 string makeUtf8StringFromWindowsString(in wchar[] str) {
1989 	char[] buffer;
1990 	auto got = WideCharToMultiByte(CP_UTF8, 0, str.ptr, cast(int) str.length, null, 0, null, null);
1991 	buffer.length = got;
1992 
1993 	// it is unique because we just allocated it above!
1994 	return cast(string) makeUtf8StringFromWindowsString(str, buffer);
1995 }
1996 
1997 /// ditto
1998 version(Windows)
1999 string makeUtf8StringFromWindowsString(wchar* str) {
2000 	char[] buffer;
2001 	auto got = WideCharToMultiByte(CP_UTF8, 0, str, -1, null, 0, null, null);
2002 	buffer.length = got;
2003 
2004 	got = WideCharToMultiByte(CP_UTF8, 0, str, -1, buffer.ptr, cast(int) buffer.length, null, null);
2005 	if(got == 0) {
2006 		if(GetLastError() == ERROR_INSUFFICIENT_BUFFER)
2007 			throw new object.Exception("not enough buffer");
2008 		else
2009 			throw new object.Exception("conversion"); // FIXME: GetLastError
2010 	}
2011 	return cast(string) buffer[0 .. got];
2012 }
2013 
2014 // only used from minigui rn
2015 package int findIndexOfZero(in wchar[] str) {
2016 	foreach(idx, wchar ch; str)
2017 		if(ch == 0)
2018 			return cast(int) idx;
2019 	return cast(int) str.length;
2020 }
2021 package int findIndexOfZero(in char[] str) {
2022 	foreach(idx, char ch; str)
2023 		if(ch == 0)
2024 			return cast(int) idx;
2025 	return cast(int) str.length;
2026 }
2027 
2028 /++
2029 	Returns a minimum buffer length to hold the string `s` with the given conversions. It might be slightly larger than necessary, but is guaranteed to be big enough to hold it.
2030 
2031 	History:
2032 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
2033 +/
2034 version(Windows)
2035 int sizeOfConvertedWstring(in char[] s, int conversionFlags) {
2036 	int size = 0;
2037 
2038 	if(conversionFlags & WindowsStringConversionFlags.convertNewLines) {
2039 		// need to convert line endings, which means the length will get bigger.
2040 
2041 		// BTW I betcha this could be faster with some simd stuff.
2042 		char last;
2043 		foreach(char ch; s) {
2044 			if(ch == 10 && last != 13)
2045 				size++; // will add a 13 before it...
2046 			size++;
2047 			last = ch;
2048 		}
2049 	} else {
2050 		// no conversion necessary, just estimate based on length
2051 		/*
2052 			I don't think there's any string with a longer length
2053 			in code units when encoded in UTF-16 than it has in UTF-8.
2054 			This will probably over allocate, but that's OK.
2055 		*/
2056 		size = cast(int) s.length;
2057 	}
2058 
2059 	if(conversionFlags & WindowsStringConversionFlags.zeroTerminate)
2060 		size++;
2061 
2062 	return size;
2063 }
2064 
2065 /++
2066 	Used by [makeWindowsString] and [WCharzBuffer]
2067 
2068 	History:
2069 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
2070 +/
2071 version(Windows)
2072 enum WindowsStringConversionFlags : int {
2073 	/++
2074 		Append a zero terminator to the string.
2075 	+/
2076 	zeroTerminate = 1,
2077 	/++
2078 		Converts newlines from \n to \r\n.
2079 	+/
2080 	convertNewLines = 2,
2081 }
2082 
2083 /++
2084 	An int printing function that doesn't need to import Phobos. Can do some of the things std.conv.to and std.format.format do.
2085 
2086 	The buffer must be sized to hold the converted number. 32 chars is enough for most anything.
2087 
2088 	Returns: the slice of `buffer` containing the converted number.
2089 +/
2090 char[] intToString(long value, char[] buffer, IntToStringArgs args = IntToStringArgs.init) {
2091 	const int radix = args.radix ? args.radix : 10;
2092 	const int digitsPad = args.padTo;
2093 	const int groupSize = args.groupSize;
2094 
2095 	int pos;
2096 
2097 	bool needsOverflowFixup = false;
2098 
2099 	if(value == long.min) {
2100 		// -long.min will overflow so we're gonna cheat
2101 		value += 1;
2102 		needsOverflowFixup = true;
2103 	}
2104 
2105 	if(value < 0) {
2106 		buffer[pos++] = '-';
2107 		value = -value;
2108 	}
2109 
2110 	int start = pos;
2111 	int digitCount;
2112 	int groupCount;
2113 
2114 	void outputDigit(char c) {
2115 		if(groupSize && groupCount == groupSize) {
2116 			buffer[pos++] = args.separator;
2117 			groupCount = 0;
2118 		}
2119 
2120 		buffer[pos++] = c;
2121 		groupCount++;
2122 		digitCount++;
2123 
2124 	}
2125 
2126 	do {
2127 		auto remainder = value % radix;
2128 		value = value / radix;
2129 		if(needsOverflowFixup) {
2130 			if(remainder + 1 == radix) {
2131 				outputDigit('0');
2132 				remainder = 0;
2133 				value += 1;
2134 			} else {
2135 				remainder += 1;
2136 			}
2137 			needsOverflowFixup = false;
2138 		}
2139 
2140 		outputDigit(cast(char) (remainder < 10 ? (remainder + '0') : (remainder - 10 + args.ten)));
2141 	} while(value);
2142 
2143 	if(digitsPad > 0) {
2144 		while(digitCount < digitsPad) {
2145 			if(groupSize && groupCount == groupSize) {
2146 				buffer[pos++] = args.separator;
2147 				groupCount = 0;
2148 			}
2149 			buffer[pos++] = args.padWith;
2150 			digitCount++;
2151 			groupCount++;
2152 		}
2153 	}
2154 
2155 	assert(pos >= 1);
2156 	assert(pos - start > 0);
2157 
2158 	auto reverseSlice = buffer[start .. pos];
2159 	for(int i = 0; i < reverseSlice.length / 2; i++) {
2160 		auto paired = cast(int) reverseSlice.length - i - 1;
2161 		char tmp = reverseSlice[i];
2162 		reverseSlice[i] = reverseSlice[paired];
2163 		reverseSlice[paired] = tmp;
2164 	}
2165 
2166 	return buffer[0 .. pos];
2167 }
2168 
2169 /// ditto
2170 struct IntToStringArgs {
2171 	private {
2172 		ubyte padTo;
2173 		char padWith;
2174 		ubyte radix;
2175 		char ten;
2176 		ubyte groupSize;
2177 		char separator;
2178 	}
2179 
2180 	IntToStringArgs withPadding(int padTo, char padWith = '0') {
2181 		IntToStringArgs args = this;
2182 		args.padTo = cast(ubyte) padTo;
2183 		args.padWith = padWith;
2184 		return args;
2185 	}
2186 
2187 	IntToStringArgs withRadix(int radix, char ten = 'a') {
2188 		IntToStringArgs args = this;
2189 		args.radix = cast(ubyte) radix;
2190 		args.ten = ten;
2191 		return args;
2192 	}
2193 
2194 	IntToStringArgs withGroupSeparator(int groupSize, char separator = '_') {
2195 		IntToStringArgs args = this;
2196 		args.groupSize = cast(ubyte) groupSize;
2197 		args.separator = separator;
2198 		return args;
2199 	}
2200 }
2201 
2202 struct FloatToStringArgs {
2203 	private {
2204 		// whole number component
2205 		ubyte padTo;
2206 		char padWith;
2207 		ubyte groupSize;
2208 		char separator;
2209 
2210 		// for the fractional component
2211 		ubyte minimumPrecision =  0; // will always show at least this many digits after the decimal (if it is 0 there may be no decimal)
2212 		ubyte maximumPrecision = 32; // will round to this many after the decimal
2213 
2214 		bool useScientificNotation; // if this is true, note the whole number component will always be exactly one digit, so the pad stuff applies to the exponent only and it assumes pad with zero's to two digits
2215 	}
2216 
2217 	FloatToStringArgs withPadding(int padTo, char padWith = '0') {
2218 		FloatToStringArgs args = this;
2219 		args.padTo = cast(ubyte) padTo;
2220 		args.padWith = padWith;
2221 		return args;
2222 	}
2223 
2224 	FloatToStringArgs withGroupSeparator(int groupSize, char separator = '_') {
2225 		FloatToStringArgs args = this;
2226 		args.groupSize = cast(ubyte) groupSize;
2227 		args.separator = separator;
2228 		return args;
2229 	}
2230 
2231 	FloatToStringArgs withPrecision(int minDigits, int maxDigits = 0) {
2232 		FloatToStringArgs args = this;
2233 		args.minimumPrecision = cast(ubyte) minDigits;
2234 		if(maxDigits < minDigits)
2235 			maxDigits = minDigits;
2236 		args.maximumPrecision = cast(ubyte) maxDigits;
2237 		return args;
2238 	}
2239 
2240 	FloatToStringArgs withScientificNotation(bool enabled) {
2241 		FloatToStringArgs args = this;
2242 		args.useScientificNotation = enabled;
2243 		return args;
2244 	}
2245 }
2246 
2247 // the buffer should be at least 32 bytes long, maybe more with other args
2248 char[] floatToString(double value, char[] buffer, FloatToStringArgs args = FloatToStringArgs.init) {
2249 	// actually doing this is pretty painful, so gonna pawn it off on the C lib
2250 	import core.stdc.stdio;
2251 	// FIXME: what if there's a locale in place that changes the decimal point?
2252 	auto ret = snprintf(buffer.ptr, buffer.length, args.useScientificNotation ? "%.*e" : "%.*f", args.maximumPrecision, value);
2253 	if(!args.useScientificNotation && (args.padTo || args.groupSize)) {
2254 		char[32] scratch = void;
2255 		auto idx = buffer[0 .. ret].indexOf(".");
2256 
2257 		int digitsOutput = 0;
2258 		int digitsGrouped = 0;
2259 		if(idx > 0) {
2260 			// there is a whole number component
2261 			int pos = cast(int) scratch.length;
2262 
2263 			auto splitPoint = idx;
2264 
2265 			while(idx) {
2266 				if(args.groupSize && digitsGrouped == args.groupSize) {
2267 					scratch[--pos] = args.separator;
2268 					digitsGrouped = 0;
2269 				}
2270 				scratch[--pos] = buffer[--idx];
2271 
2272 				digitsOutput++;
2273 				digitsGrouped++;
2274 			}
2275 
2276 			if(args.padTo)
2277 			while(digitsOutput < args.padTo) {
2278 				if(args.groupSize && digitsGrouped == args.groupSize) {
2279 					scratch[--pos] = args.separator;
2280 					digitsGrouped = 0;
2281 				}
2282 
2283 				scratch[--pos] = args.padWith;
2284 
2285 				digitsOutput++;
2286 				digitsGrouped++;
2287 			}
2288 
2289 			char[32] remainingBuffer;
2290 			remainingBuffer[0 .. ret - splitPoint]= buffer[splitPoint .. ret];
2291 
2292 			buffer[0 .. scratch.length - pos] = scratch[pos .. $];
2293 			buffer[scratch.length - pos .. scratch.length - pos + ret - splitPoint] = remainingBuffer[0 .. ret - splitPoint];
2294 
2295 			ret = cast(int) scratch.length - pos + ret - splitPoint;
2296 		}
2297 	}
2298 
2299 	// sprintf will always put zeroes on to the maximum precision, but if it is a bunch of trailing zeroes, we can trim them
2300 	// if scientific notation, don't forget to bring the e back down though.
2301 	int trailingZeroesStart = -1;
2302 	int dot = -1;
2303 	int trailingZeroesEnd;
2304 	bool inZone;
2305 	foreach(idx, ch; buffer[0 .. ret]) {
2306 		if(inZone) {
2307 			if(ch == '0') {
2308 				if(trailingZeroesStart == -1) {
2309 					trailingZeroesStart = cast(int) idx;
2310 				}
2311 			} else if(ch == 'e') {
2312 				trailingZeroesEnd = cast(int) idx;
2313 				break;
2314 			} else {
2315 				trailingZeroesStart = -1;
2316 			}
2317 		} else {
2318 			if(ch == '.') {
2319 				inZone = true;
2320 				dot = cast(int) idx;
2321 			}
2322 		}
2323 	}
2324 	if(trailingZeroesEnd == 0)
2325 		trailingZeroesEnd = ret;
2326 
2327 		// 0.430000
2328 		// end = $
2329 		// dot = 1
2330 		// start = 4
2331 		// precision is thus 3-1 = 2
2332 		// if min precision = 0
2333 	if(dot != -1 && trailingZeroesStart > dot) {
2334 		auto currentPrecision = trailingZeroesStart - dot - 1;
2335 		auto precWanted = (args.minimumPrecision > currentPrecision) ? args.minimumPrecision : currentPrecision;
2336 		auto sliceOffset = dot + precWanted + 1;
2337 		if(precWanted == 0)
2338 			sliceOffset -= 1; // remove the dot
2339 		char[] keep = buffer[trailingZeroesEnd .. ret];
2340 
2341 		// slice copy doesn't allow overlapping and since it can, we need to memmove
2342 		//buffer[sliceOffset .. sliceOffset + keep.length] = keep[];
2343 		import core.stdc.string;
2344 		memmove(buffer[sliceOffset .. ret].ptr, keep.ptr, keep.length);
2345 
2346 		ret = cast(int) (sliceOffset + keep.length);
2347 	}
2348 	/+
2349 	if(minimumPrecision > 0) {
2350 		auto idx = buffer[0 .. ret].indexOf(".");
2351 		if(idx == -1) {
2352 			buffer[ret++] = '.';
2353 			idx = ret;
2354 		}
2355 
2356 		while(ret - idx < minimumPrecision)
2357 			buffer[ret++] = '0';
2358 	}
2359 	+/
2360 	return buffer[0 .. ret];
2361 }
2362 
2363 unittest {
2364 	char[32] buffer;
2365 	assert(intToString(0, buffer[]) == "0");
2366 	assert(intToString(-1, buffer[]) == "-1");
2367 	assert(intToString(-132, buffer[]) == "-132");
2368 	assert(intToString(-1932, buffer[]) == "-1932");
2369 	assert(intToString(1, buffer[]) == "1");
2370 	assert(intToString(132, buffer[]) == "132");
2371 	assert(intToString(1932, buffer[]) == "1932");
2372 
2373 	assert(intToString(0x1, buffer[], IntToStringArgs().withRadix(16)) == "1");
2374 	assert(intToString(0x1b, buffer[], IntToStringArgs().withRadix(16)) == "1b");
2375 	assert(intToString(0xef1, buffer[], IntToStringArgs().withRadix(16)) == "ef1");
2376 
2377 	assert(intToString(0xef1, buffer[], IntToStringArgs().withRadix(16).withPadding(8)) == "00000ef1");
2378 	assert(intToString(-0xef1, buffer[], IntToStringArgs().withRadix(16).withPadding(8)) == "-00000ef1");
2379 	assert(intToString(-0xef1, buffer[], IntToStringArgs().withRadix(16, 'A').withPadding(8, ' ')) == "-     EF1");
2380 
2381 	assert(intToString(4000, buffer[], IntToStringArgs().withPadding(4).withGroupSeparator(3, ',')) == "4,000");
2382 	assert(intToString(400, buffer[], IntToStringArgs().withPadding(4).withGroupSeparator(3, ',')) == "0,400");
2383 
2384 	const pi = 3.14159256358979;
2385 	assert(floatToString(pi, buffer[], FloatToStringArgs().withPrecision(3)) == "3.142");
2386 	assert(floatToString(pi, buffer[], FloatToStringArgs().withPrecision(2)) == "3.14");
2387 	assert(floatToString(pi, buffer[], FloatToStringArgs().withPrecision(0)) == "3");
2388 
2389 	assert(floatToString(4.0, buffer[], FloatToStringArgs().withPrecision(0)) == "4");
2390 	assert(floatToString(4.0, buffer[], FloatToStringArgs().withPrecision(3)) == "4.000");
2391 
2392 	assert(floatToString(4.0, buffer[], FloatToStringArgs().withPadding(3).withPrecision(3)) == "004.000");
2393 	assert(floatToString(4.0, buffer[], FloatToStringArgs().withPadding(3).withGroupSeparator(3, ',').withPrecision(3)) == "004.000");
2394 	assert(floatToString(4.0, buffer[], FloatToStringArgs().withPadding(4).withGroupSeparator(3, ',').withPrecision(3)) == "0,004.000");
2395 	assert(floatToString(4000.0, buffer[], FloatToStringArgs().withPadding(4).withGroupSeparator(3, ',').withPrecision(3)) == "4,000.000");
2396 
2397 	assert(floatToString(4.25, buffer[], FloatToStringArgs().withPrecision(3, 5)) == "4.250");
2398 	assert(floatToString(4.25, buffer[], FloatToStringArgs().withPrecision(2, 5)) == "4.25");
2399 	assert(floatToString(4.25, buffer[], FloatToStringArgs().withPrecision(0, 5)) == "4.25");
2400 	assert(floatToString(4.25, buffer[], FloatToStringArgs().withPrecision(0)) == "4");
2401 	assert(floatToString(4.251, buffer[], FloatToStringArgs().withPrecision(1)) == "4.3"); // 2.25 would be rounded to even and thus be 2.2... sometimes. this less ambiguous
2402 
2403 	//assert(floatToString(4.25, buffer[], FloatToStringArgs().withPrecision(1)) == "4.2");
2404 	//assert(floatToString(4.35, buffer[], FloatToStringArgs().withPrecision(1)) == "4.3");
2405 	/+
2406 	import core.stdc.stdio;
2407 	printf("%.1f\n", 4.25); // 4.2
2408 	printf("%.1f\n", 4.35); // 4.3
2409 	+/
2410 
2411 	assert(floatToString(pi*10, buffer[], FloatToStringArgs().withPrecision(2).withScientificNotation(true)) == "3.14e+01");
2412 
2413 	assert(floatToString(500, buffer[], FloatToStringArgs().withPrecision(0, 2).withScientificNotation(true)) == "5e+02");
2414 }
2415 
2416 /++
2417 	History:
2418 		Moved from color.d to core.d in March 2023 (dub v11.0).
2419 +/
2420 nothrow @safe @nogc pure
2421 inout(char)[] stripInternal(return inout(char)[] s) {
2422 	bool isAllWhitespace = true;
2423 	foreach(i, char c; s)
2424 		if(c != ' ' && c != '\t' && c != '\n' && c != '\r') {
2425 			isAllWhitespace = false;
2426 			s = s[i .. $];
2427 			break;
2428 		}
2429 	if(isAllWhitespace)
2430 		return s[0 .. 0];
2431 
2432 	foreach_reverse(i, char c; s) {
2433 		if(c != ' ' && c != '\t' && c != '\n' && c != '\r') {
2434 			s = s[0 .. i + 1];
2435 			break;
2436 		}
2437 	}
2438 
2439 	return s;
2440 }
2441 
2442 /// ditto
2443 nothrow @safe @nogc pure
2444 inout(char)[] stripRightInternal(return inout(char)[] s) {
2445 	bool isAllWhitespace = true;
2446 	foreach_reverse(a, c; s) {
2447 		if(c != ' ' && c != '\t' && c != '\n' && c != '\r') {
2448 			s = s[0 .. a + 1];
2449 			isAllWhitespace = false;
2450 			break;
2451 		}
2452 	}
2453 	if(isAllWhitespace)
2454 		s = s[0..0];
2455 
2456 	return s;
2457 
2458 }
2459 
2460 /++
2461 	Shortcut for converting some types to string without invoking Phobos (but it may as a last resort).
2462 
2463 	History:
2464 		Moved from color.d to core.d in March 2023 (dub v11.0).
2465 +/
2466 string toStringInternal(T)(T t) {
2467 	char[256] bufferBacking;
2468 	return writeGuts(bufferBacking[], null, null, null, false, false, &makeString, t);
2469 	/+
2470 	char[64] buffer;
2471 	static if(is(typeof(t.toString) : string))
2472 		return t.toString();
2473 	else static if(is(T : string))
2474 		return t;
2475 	else static if(is(T == enum)) {
2476 		switch(t) {
2477 			foreach(memberName; __traits(allMembers, T)) {
2478 				case __traits(getMember, T, memberName):
2479 					return memberName;
2480 			}
2481 			default:
2482 				return "<unknown>";
2483 		}
2484 	} else static if(is(T : long)) {
2485 		return intToString(t, buffer[]).idup;
2486 	} else static if(is(T : const E[], E)) {
2487 		string ret = "[";
2488 		foreach(idx, e; t) {
2489 			if(idx)
2490 				ret ~= ", ";
2491 			ret ~= toStringInternal(e);
2492 		}
2493 		ret ~= "]";
2494 		return ret;
2495 	} else static if(is(T : double)) {
2496 		import core.stdc.stdio;
2497 		auto ret = snprintf(buffer.ptr, buffer.length, "%f", t);
2498 		return buffer[0 .. ret].idup;
2499 	} else {
2500 		static assert(0, T.stringof ~ " makes compile too slow");
2501 		// import std.conv; return to!string(t);
2502 	}
2503 	+/
2504 }
2505 
2506 unittest {
2507 	assert(toStringInternal(-43) == "-43");
2508 	assert(toStringInternal(4.5) == "4.5");
2509 }
2510 
2511 char[] toTextBuffer(T...)(char[] bufferBacking, T t) {
2512 	return cast(char[]) writeGuts(bufferBacking[], null, null, null, false, false, &makeStringCasting, t);
2513 }
2514 
2515 /++
2516 
2517 +/
2518 string flagsToString(Flags)(ulong value) {
2519 	string r;
2520 
2521 	void add(string memberName) {
2522 		if(r.length)
2523 			r ~= " | ";
2524 		r ~= memberName;
2525 	}
2526 
2527 	string none = "<none>";
2528 
2529 	foreach(memberName; __traits(allMembers, Flags)) {
2530 		auto flag = cast(ulong) __traits(getMember, Flags, memberName);
2531 		if(flag) {
2532 			if((value & flag) == flag)
2533 				add(memberName);
2534 		} else {
2535 			none = memberName;
2536 		}
2537 	}
2538 
2539 	if(r.length == 0)
2540 		r = none;
2541 
2542 	return r;
2543 }
2544 
2545 unittest {
2546 	enum MyFlags {
2547 		none = 0,
2548 		a = 1,
2549 		b = 2
2550 	}
2551 
2552 	assert(flagsToString!MyFlags(3) == "a | b");
2553 	assert(flagsToString!MyFlags(0) == "none");
2554 	assert(flagsToString!MyFlags(2) == "b");
2555 }
2556 
2557 private enum dchar replacementDchar = '\uFFFD';
2558 
2559 package size_t encodeUtf8(out char[4] buf, dchar c) @safe pure {
2560     if (c <= 0x7F)
2561     {
2562         assert(isValidDchar(c));
2563         buf[0] = cast(char) c;
2564         return 1;
2565     }
2566     if (c <= 0x7FF)
2567     {
2568         assert(isValidDchar(c));
2569         buf[0] = cast(char)(0xC0 | (c >> 6));
2570         buf[1] = cast(char)(0x80 | (c & 0x3F));
2571         return 2;
2572     }
2573     if (c <= 0xFFFF)
2574     {
2575         if (0xD800 <= c && c <= 0xDFFF)
2576             c = replacementDchar;
2577 
2578         assert(isValidDchar(c));
2579     L3:
2580         buf[0] = cast(char)(0xE0 | (c >> 12));
2581         buf[1] = cast(char)(0x80 | ((c >> 6) & 0x3F));
2582         buf[2] = cast(char)(0x80 | (c & 0x3F));
2583         return 3;
2584     }
2585     if (c <= 0x10FFFF)
2586     {
2587         assert(isValidDchar(c));
2588         buf[0] = cast(char)(0xF0 | (c >> 18));
2589         buf[1] = cast(char)(0x80 | ((c >> 12) & 0x3F));
2590         buf[2] = cast(char)(0x80 | ((c >> 6) & 0x3F));
2591         buf[3] = cast(char)(0x80 | (c & 0x3F));
2592         return 4;
2593     }
2594 
2595     assert(!isValidDchar(c));
2596     c = replacementDchar;
2597     goto L3;
2598 }
2599 
2600 /++
2601 	If it fits in the provided buffer, it will use it, otherwise, it will reallocate as-needed with the append operator.
2602 
2603 	Returns:
2604 		the slice of `buffer` actually used, or the newly allocated array, if it was necessary.
2605 	History:
2606 		Added November 14, 2025
2607 +/
2608 char[] transcodeUtf(scope const wchar[] input, char[] buffer) {
2609 	size_t pos;
2610 	char[4] temp;
2611 	foreach(dchar ch; input) {
2612 		auto stride = encodeUtf8(temp, ch);
2613 		if(pos + stride < buffer.length) {
2614 			buffer[pos .. pos + stride] = temp[0 .. stride];
2615 			pos += stride;
2616 		} else {
2617 			char[] t = buffer[0 .. pos];
2618 			t ~= temp[0 .. stride];
2619 			buffer = t;
2620 		}
2621 	}
2622 	return buffer[0 .. pos];
2623 }
2624 
2625 /// ditto
2626 char[] transcodeUtf(scope const dchar[] input, char[] buffer) {
2627 	// yes, this function body is char-for-char identical to the
2628 	// previous overload. i just don't want to use a template here.
2629 	size_t pos;
2630 	char[4] temp;
2631 	foreach(dchar ch; input) {
2632 		auto stride = encodeUtf8(temp, ch);
2633 		if(pos + stride < buffer.length) {
2634 			buffer[pos .. pos + stride] = temp[0 .. stride];
2635 			pos += stride;
2636 		} else {
2637 			char[] t = buffer[0 .. pos];
2638 			t ~= temp[0 .. stride];
2639 			buffer = t;
2640 		}
2641 	}
2642 	return buffer[0 .. pos];
2643 }
2644 
2645 inout(char)[] transcodeUtf(inout(char)[] input) {
2646 	// no change needed
2647 	return input;
2648 }
2649 
2650 private bool isValidDchar(dchar c) pure nothrow @safe @nogc
2651 {
2652     return c < 0xD800 || (c > 0xDFFF && c <= 0x10FFFF);
2653 }
2654 
2655 // technically s is octets but meh
2656 package string encodeUriComponent(string s) {
2657 	char[3] encodeChar(char c) {
2658 		char[3] buffer;
2659 		buffer[0] = '%';
2660 
2661 		enum hexchars = "0123456789ABCDEF";
2662 		buffer[1] = hexchars[c >> 4];
2663 		buffer[2] = hexchars[c & 0x0f];
2664 
2665 		return buffer;
2666 	}
2667 
2668 	string n;
2669 	size_t previous = 0;
2670 	foreach(idx, char ch; s) {
2671 		if(
2672 			(ch >= 'A' && ch <= 'Z')
2673 			||
2674 			(ch >= 'a' && ch <= 'z')
2675 			||
2676 			(ch >= '0' && ch <= '9')
2677 			|| ch == '-' || ch == '_' || ch == '.' || ch == '~' // unreserved set
2678 			|| ch == '!' || ch == '*' || ch == '\''|| ch == '(' || ch == ')' // subdelims but allowed in uri component (phobos also no encode them)
2679 		) {
2680 			// does not need encoding
2681 		} else {
2682 			n ~= s[previous .. idx];
2683 			n ~= encodeChar(ch);
2684 			previous = idx + 1;
2685 		}
2686 	}
2687 
2688 	if(n.length) {
2689 		n ~= s[previous .. $];
2690 		return n;
2691 	} else {
2692 		return s; // nothing needed encoding
2693 	}
2694 }
2695 unittest {
2696 	assert(encodeUriComponent("foo") == "foo");
2697 	assert(encodeUriComponent("f33Ao") == "f33Ao");
2698 	assert(encodeUriComponent("/") == "%2F");
2699 	assert(encodeUriComponent("/foo") == "%2Ffoo");
2700 	assert(encodeUriComponent("foo/") == "foo%2F");
2701 	assert(encodeUriComponent("foo/bar") == "foo%2Fbar");
2702 	assert(encodeUriComponent("foo/bar/") == "foo%2Fbar%2F");
2703 }
2704 
2705 // FIXME: I think if translatePlusToSpace we're supposed to do newline normalization too
2706 package string decodeUriComponent(string s, bool translatePlusToSpace = false) {
2707 	int skipping = 0;
2708 	size_t previous = 0;
2709 	string n = null;
2710 	foreach(idx, char ch; s) {
2711 		if(skipping) {
2712 			skipping--;
2713 			continue;
2714 		}
2715 
2716 		if(ch == '%') {
2717 			int hexDecode(char c) {
2718 				if(c >= 'A' && c <= 'F')
2719 					return c - 'A' + 10;
2720 				else if(c >= 'a' && c <= 'f')
2721 					return c - 'a' + 10;
2722 				else if(c >= '0' && c <= '9')
2723 					return c - '0' + 0;
2724 				else
2725 					throw ArsdException!"Invalid percent-encoding"("Invalid char encountered", idx, s);
2726 			}
2727 
2728 			skipping = 2;
2729 			n ~= s[previous .. idx];
2730 
2731 			if(idx + 2 >= s.length)
2732 				throw ArsdException!"Invalid percent-encoding"("End of string reached", idx, s);
2733 
2734 			n ~= (hexDecode(s[idx + 1]) << 4) | hexDecode(s[idx + 2]);
2735 
2736 			previous = idx + 3;
2737 		} else if(translatePlusToSpace && ch == '+') {
2738 			n ~= s[previous .. idx];
2739 			n ~= " ";
2740 			previous = idx + 1;
2741 		}
2742 	}
2743 
2744 	if(n.length) {
2745 		n ~= s[previous .. $];
2746 		return n;
2747 	} else {
2748 		return s; // nothing needed decoding
2749 	}
2750 }
2751 
2752 unittest {
2753 	assert(decodeUriComponent("foo") == "foo");
2754 	assert(decodeUriComponent("%2F") == "/");
2755 	assert(decodeUriComponent("%2f") == "/");
2756 	assert(decodeUriComponent("%2Ffoo") == "/foo");
2757 	assert(decodeUriComponent("foo%2F") == "foo/");
2758 	assert(decodeUriComponent("foo%2Fbar") == "foo/bar");
2759 	assert(decodeUriComponent("foo%2Fbar%2F") == "foo/bar/");
2760 	assert(decodeUriComponent("%2F%2F%2F") == "///");
2761 
2762 	assert(decodeUriComponent("+") == "+");
2763 	assert(decodeUriComponent("+", true) == " ");
2764 }
2765 
2766 public auto toDelegate(T)(T t) {
2767 	// static assert(is(T == function)); // lol idk how to do what i actually want here
2768 
2769 	static if(is(T Return == return))
2770 	static if(is(typeof(*T) Params == __parameters)) {
2771 		static struct Wrapper {
2772 			Return call(Params params) {
2773 				return (cast(T) &this)(params);
2774 			}
2775 		}
2776 		return &((cast(Wrapper*) t).call);
2777 	} else static assert(0, "could not get params; is it already a delegate you can pass directly?");
2778 	else static assert(0, "could not get return value, if it is a functor maybe try getting a delegate with `&yourobj.opCall` instead of toDelegate(yourobj)");
2779 }
2780 
2781 @system unittest {
2782 	int function(int) fn;
2783 	fn = (a) { return a; };
2784 
2785 	int delegate(int) dg = toDelegate(fn);
2786 
2787 	assert(dg.ptr is fn); // it stores the original function as the context pointer
2788 	assert(dg.funcptr !is fn); // which is called through a lil trampoline
2789 	assert(dg(5) == 5); // and forwards the args correctly
2790 }
2791 
2792 /++
2793 	This populates a struct from a list of values (or other expressions, but it only looks at the values) based on types of the members, with one exception: `bool` members.. maybe.
2794 
2795 	It is intended for collecting a record of relevant UDAs off a symbol in a single call like this:
2796 
2797 	---
2798 		struct Name {
2799 			string n;
2800 		}
2801 
2802 		struct Validator {
2803 			string regex;
2804 		}
2805 
2806 		struct FormInfo {
2807 			Name name;
2808 			Validator validator;
2809 		}
2810 
2811 		@Name("foo") @Validator(".*")
2812 		void foo() {}
2813 
2814 		auto info = populateFromUdas!(FormInfo, __traits(getAttributes, foo));
2815 		assert(info.name == Name("foo"));
2816 		assert(info.validator == Validator(".*"));
2817 	---
2818 
2819 	Note that instead of UDAs, you can also pass a variadic argument list and get the same result, but the function is `populateFromArgs` and you pass them as the runtime list to bypass "args cannot be evaluated at compile time" errors:
2820 
2821 	---
2822 		void foo(T...)(T t) {
2823 			auto info = populateFromArgs!(FormInfo)(t);
2824 			// assuming the call below
2825 			assert(info.name == Name("foo"));
2826 			assert(info.validator == Validator(".*"));
2827 		}
2828 
2829 		foo(Name("foo"), Validator(".*"));
2830 	---
2831 
2832 	The benefit of this over constructing the struct directly is that the arguments can be reordered or missing. Its value is diminished with named arguments in the language.
2833 +/
2834 template populateFromUdas(Struct, UDAs...) {
2835 	enum Struct populateFromUdas = () {
2836 		Struct ret;
2837 		foreach(memberName; __traits(allMembers, Struct)) {
2838 			alias memberType = typeof(__traits(getMember, Struct, memberName));
2839 			foreach(uda; UDAs) {
2840 				static if(is(memberType == PresenceOf!a, a)) {
2841 					static if(__traits(isSame, a, uda))
2842 						__traits(getMember, ret, memberName) = true;
2843 				}
2844 				else
2845 				static if(is(typeof(uda) : memberType)) {
2846 					__traits(getMember, ret, memberName) = uda;
2847 				}
2848 			}
2849 		}
2850 
2851 		return ret;
2852 	}();
2853 }
2854 
2855 /// ditto
2856 Struct populateFromArgs(Struct, Args...)(Args args) {
2857 	Struct ret;
2858 	foreach(memberName; __traits(allMembers, Struct)) {
2859 		alias memberType = typeof(__traits(getMember, Struct, memberName));
2860 		foreach(arg; args) {
2861 			static if(is(typeof(arg == memberType))) {
2862 				__traits(getMember, ret, memberName) = arg;
2863 			}
2864 		}
2865 	}
2866 
2867 	return ret;
2868 }
2869 
2870 /// ditto
2871 struct PresenceOf(alias a) {
2872 	bool there;
2873 	alias there this;
2874 }
2875 
2876 ///
2877 unittest {
2878 	enum a;
2879 	enum b;
2880 	struct Name { string name; }
2881 	struct Info {
2882 		Name n;
2883 		PresenceOf!a athere;
2884 		PresenceOf!b bthere;
2885 		int c;
2886 	}
2887 
2888 	void test() @a @Name("test") {}
2889 
2890 	auto info = populateFromUdas!(Info, __traits(getAttributes, test));
2891 	assert(info.n == Name("test")); // but present ones are in there
2892 	assert(info.athere == true); // non-values can be tested with PresenceOf!it, which works like a bool
2893 	assert(info.bthere == false);
2894 	assert(info.c == 0); // absent thing will keep the default value
2895 }
2896 
2897 /++
2898 	Declares a delegate property with several setters to allow for handlers that don't care about the arguments.
2899 
2900 	Throughout the arsd library, you will often see types of these to indicate that you can set listeners with or without arguments. If you care about the details of the callback event, you can set a delegate that declares them. And if you don't, you can set one that doesn't even declare them and it will be ignored.
2901 +/
2902 struct FlexibleDelegate(DelegateType) {
2903 	// please note that Parameters and ReturnType are public now!
2904 	static if(is(DelegateType FunctionType == delegate))
2905 	static if(is(FunctionType Parameters == __parameters))
2906 	static if(is(DelegateType ReturnType == return)) {
2907 
2908 		/++
2909 			Calls the currently set delegate.
2910 
2911 			Diagnostics:
2912 				If the callback delegate has not been set, this may cause a null pointer dereference.
2913 		+/
2914 		ReturnType opCall(Parameters args) {
2915 			return dg(args);
2916 		}
2917 
2918 		/++
2919 			Use `if(thing)` to check if the delegate is null or not.
2920 		+/
2921 		bool opCast(T : bool)() {
2922 			return dg !is null;
2923 		}
2924 
2925 		/++
2926 			These opAssign overloads are what puts the flexibility in the flexible delegate.
2927 
2928 			Bugs:
2929 				The other overloads do not keep attributes like `nothrow` on the `dg` parameter, making them unusable if `DelegateType` requires them. I consider the attributes more trouble than they're worth anyway, and the language's poor support for composing them doesn't help any. I have no need for them and thus no plans to add them in the overloads at this time.
2930 		+/
2931 		void opAssign(DelegateType dg) {
2932 			this.dg = dg;
2933 		}
2934 
2935 		/// ditto
2936 		void opAssign(ReturnType delegate() dg) {
2937 			this.dg = (Parameters ignored) => dg();
2938 		}
2939 
2940 		/// ditto
2941 		void opAssign(ReturnType function(Parameters params) dg) {
2942 			this.dg = (Parameters params) => dg(params);
2943 		}
2944 
2945 		/// ditto
2946 		void opAssign(ReturnType function() dg) {
2947 			this.dg = (Parameters ignored) => dg();
2948 		}
2949 
2950 		/// ditto
2951 		void opAssign(typeof(null) explicitNull) {
2952 			this.dg = null;
2953 		}
2954 
2955 		private DelegateType dg;
2956 	}
2957 	else static assert(0, DelegateType.stringof ~ " failed return value check");
2958 	else static assert(0, DelegateType.stringof ~ " failed parameters check");
2959 	else static assert(0, DelegateType.stringof ~ " failed delegate check");
2960 }
2961 
2962 /++
2963 
2964 +/
2965 unittest {
2966 	// you don't have to put the arguments in a struct, but i recommend
2967 	// you do as it is more future proof - you can add more info to the
2968 	// struct without breaking user code that consumes it.
2969 	struct MyEventArguments {
2970 
2971 	}
2972 
2973 	// then you declare it just adding FlexibleDelegate!() around the
2974 	// plain delegate type you'd normally use
2975 	FlexibleDelegate!(void delegate(MyEventArguments args)) callback;
2976 
2977 	// until you set it, it will be null and thus be false in any boolean check
2978 	assert(!callback);
2979 
2980 	// can set it to the properly typed thing
2981 	callback = delegate(MyEventArguments args) {};
2982 
2983 	// and now it is no longer null
2984 	assert(callback);
2985 
2986 	// or if you don't care about the args, you can leave them off
2987 	callback = () {};
2988 
2989 	// and it works if the compiler types you as a function instead of delegate too
2990 	// (which happens automatically if you don't access any local state or if you
2991 	// explicitly define it as a function)
2992 
2993 	callback = function(MyEventArguments args) { };
2994 
2995 	// can set it back to null explicitly if you ever wanted
2996 	callback = null;
2997 
2998 	// the reflection info used internally also happens to be exposed publicly
2999 	// which can actually sometimes be nice so if the language changes, i'll change
3000 	// the code to keep this working.
3001 	static assert(is(callback.ReturnType == void));
3002 
3003 	// which can be convenient if the params is an annoying type since you can
3004 	// consistently use something like this too
3005 	callback = (callback.Parameters params) {};
3006 
3007 	// check for null and call it pretty normally
3008 	if(callback)
3009 		callback(MyEventArguments());
3010 }
3011 
3012 /+
3013 	======================
3014 	ERROR HANDLING HELPERS
3015 	======================
3016 +/
3017 
3018 /+ +
3019 	arsd code shouldn't be using Exception. Really, I don't think any code should be - instead, construct an appropriate object with structured information.
3020 
3021 	If you want to catch someone else's Exception, use `catch(object.Exception e)`.
3022 +/
3023 //package deprecated struct Exception {}
3024 
3025 
3026 /++
3027 	Base class representing my exceptions. You should almost never work with this directly, but you might catch it as a generic thing. Catch it before generic `object.Exception` or `object.Throwable` in any catch chains.
3028 
3029 
3030 	$(H3 General guidelines for exceptions)
3031 
3032 	The purpose of an exception is to cancel a task that has proven to be impossible and give the programmer enough information to use at a higher level to decide what to do about it.
3033 
3034 	Cancelling a task is accomplished with the `throw` keyword. The transmission of information to a higher level is done by the language runtime. The decision point is marked by the `catch` keyword. The part missing - the job of the `Exception` class you construct and throw - is to gather the information that will be useful at a later decision point.
3035 
3036 	It is thus important that you gather as much useful information as possible and keep it in a way that the code catching the exception can still interpret it when constructing an exception. Other concerns are secondary to this to this primary goal.
3037 
3038 	With this in mind, here's some guidelines for exception handling in arsd code.
3039 
3040 	$(H4 Allocations and lifetimes)
3041 
3042 	Don't get clever with exception allocations. You don't know what the catcher is going to do with an exception and you don't want the error handling scheme to introduce its own tricky bugs. Remember, an exception object's first job is to deliver useful information up the call chain in a way this code can use it. You don't know what this code is or what it is going to do.
3043 
3044 	Keep your memory management schemes simple and let the garbage collector do its job.
3045 
3046 	$(LIST
3047 		* All thrown exceptions should be allocated with the `new` keyword.
3048 
3049 		* Members inside the exception should be value types or have infinite lifetime (that is, be GC managed).
3050 
3051 		* While this document is concerned with throwing, you might want to add additional information to an in-flight exception, and this is done by catching, so you need to know how that works too, and there is a global compiler switch that can change things, so even inside arsd we can't completely avoid its implications.
3052 
3053 		DIP1008's presence complicates things a bit on the catch side - if you catch an exception and return it from a function, remember to `ex.refcount = ex.refcount + 1;` so you don't introduce more use-after-free woes for those unfortunate souls.
3054 	)
3055 
3056 	$(H4 Error strings)
3057 
3058 	Strings can deliver useful information to people reading the message, but are often suboptimal for delivering useful information to other chunks of code. Remember, an exception's first job is to be caught by another block of code. Printing to users is a last resort; even if you want a user-readable error message, an exception is not the ideal way to deliver one since it is constructed in the guts of a failed task, without the higher level context of what the user was actually trying to do. User error messages ought to be made from information in the exception, combined with higher level knowledge. This is best done in a `catch` block, not a `throw` statement.
3059 
3060 	As such, I recommend that you:
3061 
3062 	$(LIST
3063 		* Don't concatenate error strings at the throw site. Instead, pass the data you would have used to build the string as actual data to the constructor. This lets catchers see the original data without having to try to extract it from a string. For unique data, you will likely need a unique exception type. More on this in the next section.
3064 
3065 		* Don't construct error strings in a constructor either, for the same reason. Pass the useful data up the call chain, as exception members, to the maximum extent possible. Exception: if you are passed some data with a temporary lifetime that is important enough to pass up the chain. You may `.idup` or `to!string` to preserve as much data as you can before it is lost, but still store it in a separate member of the Exception subclass object.
3066 
3067 		* $(I Do) construct strings out of public members in [getAdditionalPrintableInformation]. When this is called, the user has requested as much relevant information as reasonable in string format. Still, avoid concatenation - it lets you pass as many key/value pairs as you like to the caller. They can concatenate as needed. However, note the words "public members" - everything you do in `getAdditionalPrintableInformation` ought to also be possible for code that caught your exception via your public methods and properties.
3068 	)
3069 
3070 	$(H4 Subclasses)
3071 
3072 	Any exception with unique data types should be a unique class. Whenever practical, this should be one you write and document at the top-level of a module. But I know we get lazy - me too - and this is why in standard D we'd often fall back to `throw new Exception("some string " ~ some info)`. To help resist these urges, I offer some helper functions to use instead that better achieve the key goal of exceptions - passing structured data up a call chain - while still being convenient to write.
3073 
3074 	See: [ArsdException], [Win32Enforce]
3075 
3076 +/
3077 class ArsdExceptionBase : object.Exception {
3078 	/++
3079 		Don't call this except from other exceptions; this is essentially an abstract class.
3080 
3081 		Params:
3082 			operation = the specific operation that failed, throwing the exception
3083 	+/
3084 	package this(string operation, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
3085 		super(operation, file, line, next);
3086 	}
3087 
3088 	/++
3089 		The toString method will print out several components:
3090 
3091 		$(LIST
3092 			* The file, line, static message, and object class name from the constructor. You can access these independently with the members `file`, `line`, `msg`, and [printableExceptionName].
3093 			* The generic category codes stored with this exception
3094 			* Additional members stored with the exception child classes (e.g. platform error codes, associated function arguments)
3095 			* The stack trace associated with the exception. You can access these lines independently with `foreach` over the `info` member.
3096 		)
3097 
3098 		This is meant to be read by the developer, not end users. You should wrap your user-relevant tasks in a try/catch block and construct more appropriate error messages from context available there, using the individual properties of the exception to add richness.
3099 	+/
3100 	final override void toString(scope void delegate(in char[]) sink) const {
3101 		// class name and info from constructor
3102 		sink(printableExceptionName);
3103 		sink("@");
3104 		sink(file);
3105 		sink("(");
3106 		char[16] buffer;
3107 		sink(intToString(line, buffer[]));
3108 		sink("): ");
3109 		sink(message);
3110 
3111 		getAdditionalPrintableInformation((string name, in char[] value) {
3112 			sink("\n");
3113 			sink(name);
3114 			sink(": ");
3115 			sink(value);
3116 		});
3117 
3118 		// full stack trace, if available
3119 		if(info) {
3120 			sink("\n----------------\n");
3121 			foreach(str; info) {
3122 				sink(str);
3123 				sink("\n");
3124 			}
3125 		}
3126 	}
3127 	/// ditto
3128 	final override string toString() {
3129 		string s;
3130 		toString((in char[] chunk) { s ~= chunk; });
3131 		return s;
3132 	}
3133 
3134 	/++
3135 		Users might like to see additional information with the exception. API consumers should pull this out of properties on your child class, but the parent class might not be able to deal with the arbitrary types at runtime the children can introduce, so bringing them all down to strings simplifies that.
3136 
3137 		Overrides should always call `super.getAdditionalPrintableInformation(sink);` before adding additional information by calling the sink with other arguments afterward.
3138 
3139 		You should spare no expense in preparing this information - translate error codes, build rich strings, whatever it takes - to make the information here useful to the reader.
3140 	+/
3141 	void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const {
3142 
3143 	}
3144 
3145 	/++
3146 		This is the name of the exception class, suitable for printing. This should be static data (e.g. a string literal). Override it in subclasses.
3147 	+/
3148 	string printableExceptionName() const {
3149 		return typeid(this).name;
3150 	}
3151 
3152 	/// deliberately hiding `Throwable.msg`. Use [message] and [toString] instead.
3153 	@disable final void msg() {}
3154 
3155 	override const(char)[] message() const {
3156 		return super.msg;
3157 	}
3158 }
3159 
3160 /++
3161 
3162 +/
3163 class InvalidArgumentsException : ArsdExceptionBase {
3164 	static struct InvalidArgument {
3165 		string name;
3166 		string description;
3167 		LimitedVariant givenValue;
3168 	}
3169 
3170 	InvalidArgument[] invalidArguments;
3171 
3172 	this(InvalidArgument[] invalidArguments, string functionName = __PRETTY_FUNCTION__, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
3173 		this.invalidArguments = invalidArguments;
3174 		super(functionName, file, line, next);
3175 	}
3176 
3177 	this(string argumentName, string argumentDescription, LimitedVariant givenArgumentValue = LimitedVariant.init, string functionName = __PRETTY_FUNCTION__, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
3178 		this([
3179 			InvalidArgument(argumentName, argumentDescription, givenArgumentValue)
3180 		], functionName, file, line, next);
3181 	}
3182 
3183 	this(string argumentName, string argumentDescription, string functionName = __PRETTY_FUNCTION__, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
3184 		this(argumentName, argumentDescription, LimitedVariant.init, functionName, file, line, next);
3185 	}
3186 
3187 	override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const {
3188 		// FIXME: print the details better
3189 		foreach(arg; invalidArguments)
3190 			sink(arg.name, arg.givenValue.toString ~ " - " ~ arg.description);
3191 	}
3192 }
3193 
3194 /++
3195 	Base class for when you've requested a feature that is not available. It may not be available because it is possible, but not yet implemented, or it might be because it is impossible on your operating system.
3196 +/
3197 class FeatureUnavailableException : ArsdExceptionBase {
3198 	this(string featureName = __PRETTY_FUNCTION__, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
3199 		super(featureName, file, line, next);
3200 	}
3201 }
3202 
3203 /++
3204 	This means the feature could be done, but I haven't gotten around to implementing it yet. If you email me, I might be able to add it somewhat quickly and get back to you.
3205 +/
3206 class NotYetImplementedException : FeatureUnavailableException {
3207 	this(string featureName = __PRETTY_FUNCTION__, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
3208 		super(featureName, file, line, next);
3209 	}
3210 
3211 }
3212 
3213 /++
3214 	This means the feature is not supported by your current operating system. You might be able to get it in an update, but you might just have to find an alternate way of doing things.
3215 +/
3216 class NotSupportedException : FeatureUnavailableException {
3217 	this(string featureName, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
3218 		super(featureName, file, line, next);
3219 	}
3220 }
3221 
3222 /++
3223 	This is a generic exception with attached arguments. It is used when I had to throw something but didn't want to write a new class.
3224 
3225 	You can catch an ArsdException to get its passed arguments out.
3226 
3227 	You can pass either a base class or a string as `Type`.
3228 
3229 	See the examples for how to use it.
3230 +/
3231 template ArsdException(alias Type, DataTuple...) {
3232 	static if(DataTuple.length)
3233 		alias Parent = ArsdException!(Type, DataTuple[0 .. $-1]);
3234 	else
3235 		alias Parent = ArsdExceptionBase;
3236 
3237 	class ArsdException : Parent {
3238 		DataTuple data;
3239 
3240 		this(DataTuple data, string file = __FILE__, size_t line = __LINE__) {
3241 			this.data = data;
3242 			static if(is(Parent == ArsdExceptionBase))
3243 				super(null, file, line);
3244 			else
3245 				super(data[0 .. $-1], file, line);
3246 		}
3247 
3248 		static opCall(R...)(R r, string file = __FILE__, size_t line = __LINE__) {
3249 			return new ArsdException!(Type, DataTuple, R)(r, file, line);
3250 		}
3251 
3252 		override string printableExceptionName() const {
3253 			static if(DataTuple.length)
3254 				enum str = "ArsdException!(" ~ Type.stringof ~ ", " ~ DataTuple.stringof[1 .. $-1] ~ ")";
3255 			else
3256 				enum str = "ArsdException!" ~ Type.stringof;
3257 			return str;
3258 		}
3259 
3260 		override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const {
3261 			ArsdExceptionBase.getAdditionalPrintableInformation(sink);
3262 
3263 			foreach(idx, datum; data) {
3264 				enum int lol = cast(int) idx;
3265 				enum key = "[" ~ lol.stringof ~ "] " ~ DataTuple[idx].stringof;
3266 				sink(key, toStringInternal(datum));
3267 			}
3268 		}
3269 	}
3270 }
3271 
3272 /// This example shows how you can throw and catch the ad-hoc exception types.
3273 unittest {
3274 	// you can throw and catch by matching the string and argument types
3275 	try {
3276 		// throw it with parenthesis after the template args (it uses opCall to construct)
3277 		throw ArsdException!"Test"();
3278 		// you could also `throw new ArsdException!"test";`, but that gets harder with args
3279 		// as we'll see in the following example
3280 		assert(0); // remove from docs
3281 	} catch(ArsdException!"Test" e) { // catch it without them
3282 		// this has no useful information except for the type
3283 		// but you can catch it like this and it is still more than generic Exception
3284 	}
3285 
3286 	// an exception's job is to deliver useful information up the chain
3287 	// and you can do that easily by passing arguments:
3288 
3289 	try {
3290 		throw ArsdException!"Test"(4, "four");
3291 		// you could also `throw new ArsdException!("Test", int, string)(4, "four")`
3292 		// but now you start to see how the opCall convenience constructor simplifies things
3293 		assert(0); // remove from docs
3294 	} catch(ArsdException!("Test", int, string) e) { // catch it and use info by specifying types
3295 		assert(e.data[0] == 4); // and extract arguments like this
3296 		assert(e.data[1] == "four");
3297 	}
3298 
3299 	// a throw site can add additional information without breaking code that catches just some
3300 	// generally speaking, each additional argument creates a new subclass on top of the previous args
3301 	// so you can cast
3302 
3303 	try {
3304 		throw ArsdException!"Test"(4, "four", 9);
3305 		assert(0); // remove from docs
3306 	} catch(ArsdException!("Test", int, string) e) { // this catch still works
3307 		assert(e.data[0] == 4);
3308 		assert(e.data[1] == "four");
3309 		// but if you were to print it, all the members would be there
3310 		// import std.stdio; writeln(e); // would show something like:
3311 		/+
3312 			ArsdException!("Test", int, string, int)@file.d(line):
3313 			[0] int: 4
3314 			[1] string: four
3315 			[2] int: 9
3316 		+/
3317 		// indicating that there's additional information available if you wanted to process it
3318 
3319 		// and meanwhile:
3320 		ArsdException!("Test", int) e2 = e; // this implicit cast works thanks to the parent-child relationship
3321 		ArsdException!"Test" e3 = e; // this works too, the base type/string still matches
3322 
3323 		// so catching those types would work too
3324 	}
3325 }
3326 
3327 /++
3328 	A tagged union that holds an error code from system apis, meaning one from Windows GetLastError() or C's errno.
3329 
3330 	You construct it with `SystemErrorCode(thing)` and the overloaded constructor tags and stores it.
3331 +/
3332 struct SystemErrorCode {
3333 	///
3334 	enum Type {
3335 		errno, ///
3336 		win32 ///
3337 	}
3338 
3339 	const Type type; ///
3340 	const int code; /// You should technically cast it back to DWORD if it is a win32 code
3341 
3342 	/++
3343 		C/unix error are typed as signed ints...
3344 		Windows' errors are typed DWORD, aka unsigned...
3345 
3346 		so just passing them straight up will pick the right overload here to set the tag.
3347 	+/
3348 	this(int errno) {
3349 		this.type = Type.errno;
3350 		this.code = errno;
3351 	}
3352 
3353 	/// ditto
3354 	this(uint win32) {
3355 		this.type = Type.win32;
3356 		this.code = win32;
3357 	}
3358 
3359 	/++
3360 		Returns if the code indicated success.
3361 
3362 		Please note that many calls do not actually set a code to success, but rather just don't touch it. Thus this may only be true on `init`.
3363 	+/
3364 	bool wasSuccessful() const {
3365 		final switch(type) {
3366 			case Type.errno:
3367 				return this.code == 0;
3368 			case Type.win32:
3369 				return this.code == 0;
3370 		}
3371 	}
3372 
3373 	/++
3374 		Constructs a string containing both the code and the explanation string.
3375 	+/
3376 	string toString() const {
3377 		return "[" ~ codeAsString ~ "] " ~ errorString;
3378 	}
3379 
3380 	/++
3381 		The numeric code itself as a string.
3382 
3383 		See [errorString] for a text explanation of the code.
3384 	+/
3385 	string codeAsString() const {
3386 		char[16] buffer;
3387 		final switch(type) {
3388 			case Type.errno:
3389 				return intToString(code, buffer[]).idup;
3390 			case Type.win32:
3391 				buffer[0 .. 2] = "0x";
3392 				return buffer[0 .. 2 + intToString(cast(uint) code, buffer[2 .. $], IntToStringArgs().withRadix(16).withPadding(8)).length].idup;
3393 		}
3394 	}
3395 
3396 	/++
3397 		A text explanation of the code. See [codeAsString] for a string representation of the numeric representation.
3398 	+/
3399 	string errorString() const @trusted {
3400 		final switch(type) {
3401 			case Type.errno:
3402 				import core.stdc.string;
3403 				auto strptr = strerror(code);
3404 				auto orig = strptr;
3405 				int len;
3406 				while(*strptr++) {
3407 					len++;
3408 				}
3409 
3410 				return orig[0 .. len].idup;
3411 			case Type.win32:
3412 				version(Windows) {
3413 					wchar[256] buffer;
3414 					auto size = FormatMessageW(
3415 						FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
3416 						null,
3417 						code,
3418 						MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
3419 						buffer.ptr,
3420 						buffer.length,
3421 						null
3422 					);
3423 
3424 					return makeUtf8StringFromWindowsString(buffer[0 .. size]).stripInternal;
3425 				} else {
3426 					return null;
3427 				}
3428 		}
3429 	}
3430 }
3431 
3432 /++
3433 
3434 +/
3435 struct SavedArgument {
3436 	string name;
3437 	LimitedVariant value;
3438 }
3439 
3440 /++
3441 
3442 +/
3443 class SystemApiException : ArsdExceptionBase {
3444 	this(string msg, int originalErrorNo, scope SavedArgument[] args = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
3445 		this(msg, SystemErrorCode(originalErrorNo), args, file, line, next);
3446 	}
3447 
3448 	version(Windows)
3449 	this(string msg, DWORD windowsError, scope SavedArgument[] args = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
3450 		this(msg, SystemErrorCode(windowsError), args, file, line, next);
3451 	}
3452 
3453 	this(string msg, SystemErrorCode code, SavedArgument[] args = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
3454 		this.errorCode = code;
3455 
3456 		// discard stuff that won't fit
3457 		if(args.length > this.args.length)
3458 			args = args[0 .. this.args.length];
3459 
3460 		this.args[0 .. args.length] = args[];
3461 
3462 		super(msg, file, line, next);
3463 	}
3464 
3465 	/++
3466 
3467 	+/
3468 	const SystemErrorCode errorCode;
3469 
3470 	/++
3471 
3472 	+/
3473 	const SavedArgument[8] args;
3474 
3475 	override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const {
3476 		super.getAdditionalPrintableInformation(sink);
3477 		sink("Error code", errorCode.toString());
3478 
3479 		foreach(arg; args)
3480 			if(arg.name !is null)
3481 				sink(arg.name, arg.value.toString());
3482 	}
3483 
3484 }
3485 
3486 /++
3487 	The low level use of this would look like `throw new WindowsApiException("MsgWaitForMultipleObjectsEx", GetLastError())` but it is meant to be used from higher level things like [Win32Enforce].
3488 
3489 	History:
3490 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
3491 +/
3492 alias WindowsApiException = SystemApiException;
3493 
3494 /++
3495 	History:
3496 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
3497 +/
3498 alias ErrnoApiException = SystemApiException;
3499 
3500 /++
3501 	Calls the C API function `fn`. If it returns an error value, it throws an [ErrnoApiException] (or subclass) after getting `errno`.
3502 +/
3503 template ErrnoEnforce(alias fn, alias errorValue = void) {
3504 	static if(is(typeof(fn) Return == return))
3505 	static if(is(typeof(fn) Params == __parameters)) {
3506 		static if(is(errorValue == void)) {
3507 			static if(is(typeof(null) : Return))
3508 				enum errorValueToUse = null;
3509 			else static if(is(Return : long))
3510 				enum errorValueToUse = -1;
3511 			else
3512 				static assert(0, "Please pass the error value");
3513 		} else {
3514 			enum errorValueToUse = errorValue;
3515 		}
3516 
3517 		Return ErrnoEnforce(Params params, ArgSentinel sentinel = ArgSentinel.init, string file = __FILE__, size_t line = __LINE__) {
3518 			import core.stdc.errno;
3519 
3520 			Return value = fn(params);
3521 
3522 			if(value == errorValueToUse) {
3523 				SavedArgument[] args; // FIXME
3524 				/+
3525 				static foreach(idx; 0 .. Params.length)
3526 					args ~= SavedArgument(
3527 						__traits(identifier, Params[idx .. idx + 1]),
3528 						params[idx]
3529 					);
3530 				+/
3531 				throw new ErrnoApiException(__traits(identifier, fn), errno, args, file, line);
3532 			}
3533 
3534 			return value;
3535 		}
3536 	}
3537 }
3538 
3539 version(Windows) {
3540 	/++
3541 		Calls the Windows API function `fn`. If it returns an error value, it throws a [WindowsApiException] (or subclass) after calling `GetLastError()`.
3542 	+/
3543 	template Win32Enforce(alias fn, alias errorValue = void) {
3544 		static if(is(typeof(fn) Return == return))
3545 		static if(is(typeof(fn) Params == __parameters)) {
3546 			static if(is(errorValue == void)) {
3547 				static if(is(Return == BOOL))
3548 					enum errorValueToUse = false;
3549 				else static if(is(Return : HANDLE))
3550 					enum errorValueToUse = NULL;
3551 				else static if(is(Return == DWORD))
3552 					enum errorValueToUse = cast(DWORD) 0xffffffff;
3553 				else
3554 					static assert(0, "Please pass the error value");
3555 			} else {
3556 				enum errorValueToUse = errorValue;
3557 			}
3558 
3559 			Return Win32Enforce(Params params, ArgSentinel sentinel = ArgSentinel.init, string file = __FILE__, size_t line = __LINE__) {
3560 				Return value = fn(params);
3561 
3562 				if(value == errorValueToUse) {
3563 					auto error = GetLastError();
3564 					SavedArgument[] args; // FIXME
3565 					throw new WindowsApiException(__traits(identifier, fn), error, args, file, line);
3566 				}
3567 
3568 				return value;
3569 			}
3570 		}
3571 	}
3572 
3573 }
3574 
3575 /+
3576 	===============
3577 	EVENT LOOP CORE
3578 	===============
3579 +/
3580 
3581 /+
3582 	UI threads
3583 		need to get window messages in addition to all the other jobs
3584 	I/O Worker threads
3585 		need to get commands for read/writes, run them, and send the reply back. not necessary on Windows
3586 		if interrupted, check cancel flags.
3587 	CPU Worker threads
3588 		gets functions, runs them, send reply back. should send a cancel flag to periodically check
3589 	Task worker threads
3590 		runs fibers and multiplexes them
3591 
3592 
3593 	General procedure:
3594 		issue the read/write command
3595 		if it would block on linux, epoll associate it. otherwise do the callback immediately
3596 
3597 		callbacks have default affinity to the current thread, meaning their callbacks always run here
3598 		accepts can usually be dispatched to any available thread tho
3599 
3600 	//  In other words, a single thread can be associated with, at most, one I/O completion port.
3601 
3602 	Realistically, IOCP only used if there is no thread affinity. If there is, just do overlapped w/ sleepex.
3603 
3604 
3605 	case study: http server
3606 
3607 	1) main thread starts the server. it does an accept loop with no thread affinity. the main thread does NOT check the global queue (the iocp/global epoll)
3608 	2) connections come in and are assigned to first available thread via the iocp/global epoll
3609 	3) these run local event loops until the connection task is finished
3610 
3611 	EVENT LOOP TYPES:
3612 		1) main ui thread - MsgWaitForMultipleObjectsEx / epoll on the local ui. it does NOT check the any worker thread thing!
3613 			The main ui thread should never terminate until the program is ready to close.
3614 			You can have additional ui threads in theory but im not really gonna support that in full; most things will assume there is just the one. simpledisplay's gui thread is the primary if it exists. (and sdpy will prolly continue to be threaded the way it is now)
3615 
3616 			The biggest complication is the TerminalDirectToEmulator, where the primary ui thread is NOT the thread that runs `main`
3617 		2) worker thread GetQueuedCompletionStatusEx / epoll on the local thread fd and the global epoll fd
3618 		3) local event loop - check local things only. SleepEx / epoll on local thread fd. This more of a compatibility hack for `waitForCompletion` outside a fiber.
3619 
3620 		i'll use:
3621 			* QueueUserAPC to send interruptions to a worker thread
3622 			* PostQueuedCompletionStatus is to send interruptions to any available thread.
3623 			* PostMessage to a window
3624 			* ??? to a fiber task
3625 
3626 		I also need a way to de-duplicate events in the queue so if you try to push the same thing it won't trigger multiple times.... I might want to keep a duplicate of the thing... really, what I'd do is post the "event wake up" message and keep the queue in my own thing. (WM_PAINT auto-coalesces)
3627 
3628 		Destructors need to be able to post messages back to a specific task to queue thread-affinity cleanup. This must be GC safe.
3629 
3630 		A task might want to wait on certain events. If the task is a fiber, it yields and gets called upon the event. If the task is a thread, it really has to call the event loop... which can be a loop of loops we want to avoid. `waitForCompletion` is more often gonna be used just to run the loop at top level tho... it might not even check for the global info availability so it'd run the local thing only.
3631 
3632 		APCs should not themselves enter an alterable wait cuz it can stack overflow. So generally speaking, they should avoid calling fibers or other event loops.
3633 +/
3634 
3635 /++
3636 	You can also pass a handle to a specific thread, if you have one.
3637 +/
3638 enum ThreadToRunIn {
3639 	/++
3640 		The callback should be only run by the same thread that set it.
3641 	+/
3642 	CurrentThread,
3643 	/++
3644 		The UI thread is a special one - it is the supervisor of the workers and the controller of gui and console handles. It is the first thread to call [arsd_core_init] actively running an event loop unless there is a thread that has actively asserted the ui supervisor role. FIXME is this true after i implemen it?
3645 
3646 		A ui thread should be always quickly responsive to new events.
3647 
3648 		There should only be one main ui thread, in which simpledisplay and minigui can be used.
3649 
3650 		Other threads can run like ui threads, but are considered temporary and only concerned with their own needs (it is the default style of loop
3651 		for an undeclared thread but will not receive messages from other threads unless there is no other option)
3652 
3653 
3654 		Ad-Hoc thread - something running an event loop that isn't another thing
3655 		Controller thread - running an explicit event loop instance set as not a task runner or blocking worker
3656 		UI thread - simpledisplay's event loop, which it will require remain live for the duration of the program (running two .eventLoops without a parent EventLoop instance will become illegal, throwing at runtime if it happens telling people to change their code)
3657 
3658 		Windows HANDLES will always be listened on the thread itself that is requesting, UNLESS it is a worker/helper thread, in which case it goes to a coordinator thread. since it prolly can't rely on the parent per se this will have to be one created by arsd core init, UNLESS the parent is inside an explicit EventLoop structure.
3659 
3660 		All use the MsgWaitForMultipleObjectsEx pattern
3661 
3662 
3663 	+/
3664 	UiThread,
3665 	/++
3666 		The callback can be called from any available worker thread. It will be added to a global queue and the first thread to see it will run it.
3667 
3668 		These will not run on the UI thread unless there is no other option on the platform (and all platforms this lib supports have other options).
3669 
3670 		These are expected to run cooperatively multitasked things; functions that frequently yield as they wait on other tasks. Think a fiber.
3671 
3672 		A task runner should be generally responsive to new events.
3673 	+/
3674 	AnyAvailableTaskRunnerThread,
3675 	/++
3676 		These are expected to run longer blocking, but independent operations. Think an individual function with no context.
3677 
3678 		A blocking worker can wait hundreds of milliseconds between checking for new events.
3679 	+/
3680 	AnyAvailableBlockingWorkerThread,
3681 	/++
3682 		The callback will be duplicated across all threads known to the arsd.core event loop.
3683 
3684 		It adds it to an immutable queue that each thread will go through... might just replace with an exit() function.
3685 
3686 
3687 		so to cancel all associated tasks for like a web server, it could just have the tasks atomicAdd to a counter and subtract when they are finished. Then you have a single semaphore you signal the number of times you have an active thing and wait for them to acknowledge it.
3688 
3689 		threads should report when they start running the loop and they really should report when they terminate but that isn't reliable
3690 
3691 
3692 		hmmm what if: all user-created threads (the public api) count as ui threads. only ones created in here are task runners or helpers. ui threads can wait on a global event to exit.
3693 
3694 		there's still prolly be one "the" ui thread, which does the handle listening on windows and is the one sdpy wants.
3695 	+/
3696 	BroadcastToAllThreads,
3697 }
3698 
3699 /++
3700 	Initializes the arsd core event loop and creates its worker threads. You don't actually have to call this, since the first use of an arsd.core function that requires it will call it implicitly, but calling it yourself gives you a chance to control the configuration more explicitly if you want to.
3701 +/
3702 void arsd_core_init(int numberOfWorkers = 0) {
3703 
3704 }
3705 
3706 version(Windows)
3707 class WindowsHandleReader_ex {
3708 	// Windows handles are always dispatched to the main ui thread, which can then send a command back to a worker thread to run the callback if needed
3709 	this(HANDLE handle) {}
3710 }
3711 
3712 version(Posix)
3713 class PosixFdReader_ex {
3714 	// posix readers can just register with whatever instance we want to handle the callback
3715 }
3716 
3717 /++
3718 
3719 +/
3720 interface ICoreEventLoop {
3721 	/++
3722 		Runs the event loop for this thread until the `until` delegate returns `true`.
3723 	+/
3724 	final void run(scope bool delegate() until) {
3725 		exitApplicationRequested = false;
3726 		while(!exitApplicationRequested && !until()) {
3727 			runOnce();
3728 		}
3729 	}
3730 
3731 	package static int function() getTimeout;
3732 	private __gshared bool exitApplicationRequested;
3733 
3734 	final static void exitApplication() {
3735 		exitApplicationRequested = true;
3736 		// FIXME: wake up all the threads
3737 	}
3738 
3739 	/++
3740 		Returns details from a call to [runOnce]. Use the named methods here for details, or it can be used in a `while` loop directly thanks to its `opCast` automatic conversion to `bool`.
3741 
3742 		History:
3743 			Added December 28, 2023
3744 	+/
3745 	static struct RunOnceResult {
3746 		enum Possibilities {
3747 			CarryOn,
3748 			LocalExit,
3749 			GlobalExit,
3750 			Interrupted
3751 
3752 		}
3753 		Possibilities result;
3754 
3755 		/++
3756 			Returns `true` if the event loop should generally continue.
3757 
3758 			Might be false if the local loop was exited or if the application is supposed to exit. If this is `false`, check [applicationExitRequested] to determine if you should move on to other work or start your final cleanup process.
3759 		+/
3760 		bool shouldContinue() const {
3761 			return result == Possibilities.CarryOn;
3762 		}
3763 
3764 		/++
3765 			Returns `true` if [ICoreEventLoop.exitApplication] was called during this event, or if the user or operating system has requested the application exit.
3766 
3767 			Details might be available through other means.
3768 		+/
3769 		bool applicationExitRequested() const {
3770 			return result == Possibilities.GlobalExit;
3771 		}
3772 
3773 		/++
3774 			Returns [shouldContinue] when used in a context for an implicit bool (e.g. `if` statements).
3775 		+/
3776 		bool opCast(T : bool)() const {
3777 			reutrn shouldContinue();
3778 		}
3779 	}
3780 
3781 	/++
3782 		Runs a single iteration of the event loop for this thread. It will return when the first thing happens, but that thing might be totally uninteresting to anyone, or it might trigger significant work you'll wait on.
3783 
3784 		Note that running this externally instead of `run` gives only the $(I illusion) of control. You're actually better off setting a recurring timer if you need things to run on a clock tick, or a single-shot timer for a one time event. They're more likely to be called on schedule inside this function than outside it.
3785 
3786 		Parameters:
3787 			timeout = a timeout value for an idle loop. There is no guarantee you won't return earlier or later than this; the function might run longer than the timeout if it has work to do. Pass `Duration.max` (the default) for an infinite duration timeout (but remember, once it finds work to do, including a false-positive wakeup or interruption by the operating system, it will return early anyway).
3788 
3789 		History:
3790 			Prior to December 28, 2023, it returned `void` and took no arguments. This change is breaking, but since the entire module is documented as unstable, it was permitted to happen as that document provided prior notice.
3791 	+/
3792 	RunOnceResult runOnce(Duration timeout = Duration.max);
3793 
3794 	/++
3795 		Adds a delegate to be called on each loop iteration, called based on the `timingFlags`.
3796 
3797 
3798 		The order in which the delegates are called is undefined and may change with each iteration of the loop. Additionally, when and how many times a loop iterates is undefined; multiple events might be handled by each iteration, or sometimes, nothing will be handled and it woke up spuriously. Your delegates need to be ok with all of this.
3799 
3800 		Parameters:
3801 			dg = the delegate to call
3802 			timingFlags =
3803 				0: never actually run the function; it can assert error if you pass this
3804 				1: run before each loop OS wait call
3805 				2: run after each loop OS wait call
3806 				3: run both before and after each OS wait call
3807 				4: single shot? NOT IMPLEMENTED
3808 				8: no-coalesce? NOT IMPLEMENTED (if after was just run, it will skip the before loops unless this flag is set)
3809 
3810 		FIXME: it should return a handle you can use to unregister it
3811 	+/
3812 	UnregisterToken addDelegateOnLoopIteration(void delegate() dg, uint timingFlags);
3813 
3814 	final UnregisterToken addDelegateOnLoopIteration(void function() dg, uint timingFlags) {
3815 		if(timingFlags == 0)
3816 			assert(0, "would never run");
3817 		return addDelegateOnLoopIteration(toDelegate(dg), timingFlags);
3818 	}
3819 
3820 	// to send messages between threads, i'll queue up a function that just call dispatchMessage. can embed the arg inside the callback helper prolly.
3821 	// tho i might prefer to actually do messages w/ run payloads so it is easier to deduplicate i can still dedupe by insepcting the call args so idk
3822 
3823 	version(Posix) {
3824 		@mustuse
3825 		static struct UnregisterToken {
3826 			private CoreEventLoopImplementation impl;
3827 			private int fd = -1;
3828 			private CallbackHelper cb;
3829 			private void delegate() dg;
3830 
3831 			/++
3832 				Unregisters the file descriptor from the event loop and releases the reference to the callback held by the event loop (which will probably free it).
3833 
3834 				You must call this when you're done. Normally, this will be right before you close the fd (Which is often after the other side closes it, meaning you got a 0 length read).
3835 			+/
3836 			void unregister() {
3837 				assert(impl !is null, "Cannot reuse unregister token");
3838 
3839 				if(dg !is null)
3840 					impl.unregisterDg(dg);
3841 
3842 				version(Arsd_core_epoll) {
3843 					if(fd != -1)
3844 						impl.unregisterFd(fd);
3845 				} else version(Arsd_core_dispatch) {
3846 					throw new NotYetImplementedException();
3847 				} else version(Arsd_core_kqueue) {
3848 					// intentionally blank - all registrations are one-shot there
3849 					// FIXME: actually it might not have gone off yet, in that case we do need to delete the filter
3850 				} else version(EmptyCoreEvent) {
3851 
3852 				}
3853 				else static assert(0);
3854 
3855 				if(cb)
3856 					cb.release();
3857 				this = typeof(this).init;
3858 			}
3859 		}
3860 
3861 		@mustuse
3862 		static struct RearmToken {
3863 			private bool readable;
3864 			private CoreEventLoopImplementation impl;
3865 			private int fd;
3866 			private CallbackHelper cb;
3867 			private uint flags;
3868 
3869 			/++
3870 				Calls [UnregisterToken.unregister]
3871 			+/
3872 			void unregister() {
3873 				assert(impl !is null, "cannot reuse rearm token after unregistering it");
3874 
3875 				version(Arsd_core_epoll) {
3876 					impl.unregisterFd(fd);
3877 				} else version(Arsd_core_dispatch) {
3878 					throw new NotYetImplementedException();
3879 				} else version(Arsd_core_kqueue) {
3880 					// intentionally blank - all registrations are one-shot there
3881 					// FIXME: actually it might not have gone off yet, in that case we do need to delete the filter
3882 				} else version(EmptyCoreEvent) {
3883 
3884 				} else static assert(0);
3885 
3886 				cb.release();
3887 				this = typeof(this).init;
3888 			}
3889 
3890 			/++
3891 				Rearms the event so you will get another callback next time it is ready.
3892 			+/
3893 			void rearm() {
3894 				assert(impl !is null, "cannot reuse rearm token after unregistering it");
3895 				impl.rearmFd(this);
3896 			}
3897 		}
3898 
3899 		UnregisterToken addCallbackOnFdReadable(int fd, CallbackHelper cb);
3900 		RearmToken addCallbackOnFdReadableOneShot(int fd, CallbackHelper cb);
3901 		RearmToken addCallbackOnFdWritableOneShot(int fd, CallbackHelper cb);
3902 	}
3903 
3904 	version(Windows) {
3905 		@mustuse
3906 		static struct UnregisterToken {
3907 			private CoreEventLoopImplementation impl;
3908 			private HANDLE handle;
3909 			private CallbackHelper cb;
3910 			private void delegate() dg;
3911 
3912 			/++
3913 				Unregisters the handle from the event loop and releases the reference to the callback held by the event loop (which will probably free it).
3914 
3915 				You must call this when you're done. Normally, this will be right before you close the handle.
3916 			+/
3917 			void unregister() {
3918 				assert(impl !is null, "Cannot reuse unregister token");
3919 
3920 				if(dg !is null)
3921 					impl.unregisterDg(dg);
3922 
3923 				if(handle)
3924 					impl.unregisterHandle(handle, cb);
3925 
3926 				if(cb)
3927 					cb.release();
3928 				this = typeof(this).init;
3929 			}
3930 		}
3931 
3932 		UnregisterToken addCallbackOnHandleReady(HANDLE handle, CallbackHelper cb);
3933 	}
3934 }
3935 
3936 /++
3937 	Get the event loop associated with this thread
3938 +/
3939 ICoreEventLoop getThisThreadEventLoop(EventLoopType type = EventLoopType.AdHoc) {
3940 	static ICoreEventLoop loop;
3941 	if(loop is null)
3942 		loop = new CoreEventLoopImplementation();
3943 	return loop;
3944 }
3945 
3946 /++
3947 	The internal types that will be exposed through other api things.
3948 +/
3949 package(arsd) enum EventLoopType {
3950 	/++
3951 		The event loop is being run temporarily and the thread doesn't promise to keep running it.
3952 	+/
3953 	AdHoc,
3954 	/++
3955 		The event loop struct has been instantiated at top level. Its destructor will run when the
3956 		function exits, which is only at the end of the entire block of work it is responsible for.
3957 
3958 		It must be in scope for the whole time the arsd event loop functions are expected to be used
3959 		(meaning it should generally be top-level in `main`)
3960 	+/
3961 	Explicit,
3962 	/++
3963 		A specialization of `Explicit`, so all the same rules apply there, but this is specifically the event loop coming from simpledisplay or minigui. It will run for the duration of the UI's existence.
3964 	+/
3965 	Ui,
3966 	/++
3967 		A special event loop specifically for threads that listen to the task runner queue and handle I/O events from running tasks. Typically, a task runner runs cooperatively multitasked coroutines (so they prefer not to block the whole thread).
3968 	+/
3969 	TaskRunner,
3970 	/++
3971 		A special event loop specifically for threads that listen to the helper function request queue. Helper functions are expected to run independently for a somewhat long time (them blocking the thread for some time is normal) and send a reply message back to the requester.
3972 	+/
3973 	HelperWorker
3974 }
3975 
3976 /+
3977 	Tasks are given an object to talk to their parent... can be a dialog where it is like
3978 
3979 	sendBuffer
3980 	waitForWordToProceed
3981 
3982 	in a loop
3983 
3984 
3985 	Tasks are assigned to a worker thread and may share it with other tasks.
3986 +/
3987 
3988 /+
3989 private ThreadLocalGcRoots gcRoots;
3990 
3991 private struct ThreadLocalGcRoots {
3992 	// it actually would be kinda cool if i could tell the GC
3993 	// that only part of this array is actually used so it can skip
3994 	// scanning the rest. but meh.
3995 	const(void)*[] roots;
3996 
3997 	void* add(const(void)* what) {
3998 		roots ~= what;
3999 		return &roots[$-1];
4000 	}
4001 }
4002 +/
4003 
4004 // the GC may not be able to see this! remember, it can be hidden inside kernel buffers
4005 package(arsd) class CallbackHelper {
4006 	import core.memory;
4007 
4008 	void call() {
4009 		if(callback)
4010 			callback();
4011 	}
4012 
4013 	void delegate() callback;
4014 	void*[3] argsStore;
4015 
4016 	void addref() {
4017 		version(HasThread)
4018 		atomicOp!"+="(refcount, 1);
4019 	}
4020 
4021 	void release() {
4022 		version(HasThread)
4023 		if(atomicOp!"-="(refcount, 1) <= 0) {
4024 			if(flags & 1)
4025 				GC.removeRoot(cast(void*) this);
4026 		}
4027 	}
4028 
4029 	private shared(int) refcount;
4030 	private uint flags;
4031 
4032 	this(void function() callback) {
4033 		this( () { callback(); } );
4034 	}
4035 
4036 	this(void delegate() callback, bool addRoot = true) {
4037 		version(HasThread)
4038 		if(addRoot) {
4039 			GC.addRoot(cast(void*) this);
4040 			this.flags |= 1;
4041 		}
4042 
4043 		this.addref();
4044 		this.callback = callback;
4045 	}
4046 }
4047 
4048 inout(char)[] trimSlashesRight(inout(char)[] txt) {
4049 	//if(txt.length && (txt[0] == '/' || txt[0] == '\\'))
4050 		//txt = txt[1 .. $];
4051 
4052 	if(txt.length && (txt[$-1] == '/' || txt[$-1] == '\\'))
4053 		txt = txt[0 .. $-1];
4054 
4055 	return txt;
4056 }
4057 
4058 enum TreatAsWindowsPath {
4059 	guess,
4060 	ifVersionWindows,
4061 	yes,
4062 	no,
4063 }
4064 
4065 // FIXME add uri from cgi/http2 and make sure the relative methods are reasonable compatible
4066 
4067 /++
4068 	This represents a file. Technically, file paths aren't actually strings (for example, on Linux, they need not be valid utf-8, while a D string is supposed to be), even though we almost always use them like that.
4069 
4070 	This type is meant to represent a filename / path. I might not keep it around.
4071 +/
4072 struct FilePath {
4073 	private string path;
4074 
4075 	this(string path) {
4076 		this.path = path;
4077 	}
4078 
4079 	bool isNull() const {
4080 		return path is null;
4081 	}
4082 
4083 	bool opCast(T:bool)() const {
4084 		return !isNull;
4085 	}
4086 
4087 	string toString() const {
4088 		return path;
4089 	}
4090 
4091 	//alias toString this;
4092 
4093 	/+ +++++++++++++++++ +/
4094 	/+  String analysis  +/
4095 	/+ +++++++++++++++++ +/
4096 
4097 	FilePath makeAbsolute(FilePath base, TreatAsWindowsPath treatAsWindowsPath = TreatAsWindowsPath.guess) const {
4098 		if(base.path.length == 0)
4099 			return this.removeExtraParts();
4100 		if(base.path[$-1] != '/' && base.path[$-1] != '\\')
4101 			base.path ~= '/';
4102 
4103 		bool isWindowsPath;
4104 		final switch(treatAsWindowsPath) {
4105 			case TreatAsWindowsPath.guess:
4106 			case TreatAsWindowsPath.yes:
4107 				isWindowsPath = true;
4108 			break;
4109 			case TreatAsWindowsPath.no:
4110 				isWindowsPath = false;
4111 			break;
4112 			case TreatAsWindowsPath.ifVersionWindows:
4113 				version(Windows)
4114 					isWindowsPath = true;
4115 				else
4116 					isWindowsPath = false;
4117 			break;
4118 		}
4119 		if(isWindowsPath) {
4120 			if(this.isUNC)
4121 				return this.removeExtraParts();
4122 			if(this.driveName)
4123 				return this.removeExtraParts();
4124 			if(this.path.length >= 1 && (this.path[0] == '/' || this.path[0] == '\\')) {
4125 				// drive-relative path, take the drive from the base
4126 				return FilePath(base.driveName ~ this.path).removeExtraParts();
4127 			}
4128 			// otherwise, take the dir name from the base and add us onto it
4129 			return FilePath(base.directoryName ~ this.path).removeExtraParts();
4130 		} else {
4131 			if(this.path.length >= 1 && this.path[0] == '/')
4132 				return this.removeExtraParts();
4133 			else
4134 				return FilePath(base.directoryName ~ this.path).removeExtraParts();
4135 		}
4136 	}
4137 
4138 	/+
4139 	FilePath makeRelative(FilePath base, TreatAsWindowsPath treatAsWindowsPath = TreatAsWindowsPath.guess) const {
4140 		if(this.path.startsWith(base.path)) {
4141 			auto p = this.path[base.path .. $];
4142 			if(p.length && p[0] == '/')
4143 				p = p[1 .. $];
4144 			if(p.length)
4145 				return FilePath(p);
4146 		}
4147 		throw new Exception("idk how to make " ~ this.path ~ " relative to " ~ base.path);
4148 	}
4149 	+/
4150 
4151 	// dg returns true to continue, false to break
4152 	void foreachPathComponent(scope bool delegate(size_t index, in char[] component) dg) const {
4153 		size_t start;
4154 		size_t skip;
4155 		if(isUNC()) {
4156 			dg(start, this.path[start .. 2]);
4157 			start = 2;
4158 			skip = 2;
4159 		}
4160 		foreach(idx, ch; this.path) {
4161 			if(skip) { skip--; continue; }
4162 			if(ch == '/' || ch == '\\') {
4163 				if(!dg(start, this.path[start .. idx + 1]))
4164 					return;
4165 				start = idx + 1;
4166 			}
4167 		}
4168 		if(start != path.length)
4169 			dg(start, this.path[start .. $]);
4170 	}
4171 
4172 	// remove cases of // or /. or /.. Only valid to call this on an absolute path.
4173 	private FilePath removeExtraParts() const {
4174 		bool changeNeeded;
4175 		foreachPathComponent((idx, component) {
4176 			auto name = component.trimSlashesRight;
4177 			if(name.length == 0 && idx != 0)
4178 				changeNeeded = true;
4179 			if(name == "." || name == "..")
4180 				changeNeeded = true;
4181 			return !changeNeeded;
4182 		});
4183 
4184 		if(!changeNeeded)
4185 			return this;
4186 
4187 		string newPath;
4188 		foreachPathComponent((idx, component) {
4189 			auto name = component.trimSlashesRight;
4190 			if(component == `\\`) // must preserve unc paths
4191 				newPath ~= component;
4192 			else if(name.length == 0 && idx != 0)
4193 				{}
4194 			else if(name == ".")
4195 				{}
4196 			else if(name == "..") {
4197 				// remove the previous component, unless it is the first component
4198 				auto sofar = FilePath(newPath);
4199 				size_t previousComponentIndex;
4200 				sofar.foreachPathComponent((idx2, component2) {
4201 					if(idx2 != newPath.length)
4202 						previousComponentIndex = idx2;
4203 					return true;
4204 				});
4205 
4206 				if(previousComponentIndex && previousComponentIndex != newPath.length) {
4207 					newPath = newPath[0 .. previousComponentIndex];
4208 					//newPath.assumeSafeAppend();
4209 				}
4210 			} else {
4211 				newPath ~= component;
4212 			}
4213 
4214 			return true;
4215 		});
4216 
4217 		return FilePath(newPath);
4218 	}
4219 
4220 	// assuming we're looking at a Windows path...
4221 	bool isUNC() const {
4222 		return (path.length > 2 && path[0 .. 2] == `\\`);
4223 	}
4224 
4225 	// assuming we're looking at a Windows path...
4226 	string driveName() const {
4227 		if(path.length < 2)
4228 			return null;
4229 		if((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')) {
4230 			if(path[1] == ':') {
4231 				if(path.length == 2 || path[2] == '\\' || path[2] == '/')
4232 					return path[0 .. 2];
4233 			}
4234 		}
4235 		return null;
4236 	}
4237 
4238 	/+
4239 	bool isAbsolute() {
4240 		if(path.length && path[0] == '/')
4241 			return true;
4242 
4243 	}
4244 
4245 	FilePath relativeTo() {
4246 
4247 	}
4248 
4249 	bool matchesGlobPattern(string globPattern) {
4250 
4251 	}
4252 
4253 	this(string directoryName, string filename) {}
4254 	this(string directoryName, string basename, string extension) {}
4255 
4256 	// remove ./, ../, stuff like that
4257 	FilePath normalize(FilePath relativeTo) {}
4258 	+/
4259 
4260 	/++
4261 		Returns the path with the directory cut off.
4262 	+/
4263 	string filename() {
4264 		foreach_reverse(idx, ch; path) {
4265 			if(ch == '\\' || ch == '/')
4266 				return path[idx + 1 .. $];
4267 		}
4268 		return path;
4269 	}
4270 
4271 	/++
4272 		Returns the path with the filename cut off.
4273 	+/
4274 	string directoryName() {
4275 		auto fn = this.filename();
4276 		if(fn is path)
4277 			return null;
4278 		return path[0 .. $ - fn.length];
4279 	}
4280 
4281 	/++
4282 		Returns the file extension, if present, including the last dot.
4283 	+/
4284 	string extension() {
4285 		foreach_reverse(idx, ch; path) {
4286 			if(ch == '.')
4287 				return path[idx .. $];
4288 		}
4289 		return null;
4290 	}
4291 
4292 	/++
4293 		Guesses the media (aka mime) content type from the file extension for this path.
4294 
4295 		Only has a few things supported. Returns null if it doesn't know.
4296 
4297 		History:
4298 			Moved from arsd.cgi to arsd.core.FilePath on October 28, 2024
4299 	+/
4300 	string contentTypeFromFileExtension() {
4301 		switch(this.extension) {
4302 			// images
4303 			case ".png":
4304 				return "image/png";
4305 			case ".apng":
4306 				return "image/apng";
4307 			case ".svg":
4308 				return "image/svg+xml";
4309 			case ".jpg":
4310 			case ".jpeg":
4311 				return "image/jpeg";
4312 
4313 			case ".txt":
4314 				return "text/plain";
4315 
4316 			case ".html":
4317 				return "text/html";
4318 			case ".css":
4319 				return "text/css";
4320 			case ".js":
4321 				return "application/javascript";
4322 			case ".wasm":
4323 				return "application/wasm";
4324 
4325 			case ".mp3":
4326 				return "audio/mpeg";
4327 
4328 			case ".pdf":
4329 				return "application/pdf";
4330 
4331 			default:
4332 				return null;
4333 		}
4334 	}
4335 }
4336 
4337 unittest {
4338 	FilePath fn;
4339 
4340 	fn = FilePath("dir/name.ext");
4341 	assert(fn.directoryName == "dir/");
4342 	assert(fn.filename == "name.ext");
4343 	assert(fn.extension == ".ext");
4344 
4345 	fn = FilePath(null);
4346 	assert(fn.directoryName is null);
4347 	assert(fn.filename is null);
4348 	assert(fn.extension is null);
4349 
4350 	fn = FilePath("file.txt");
4351 	assert(fn.directoryName is null);
4352 	assert(fn.filename == "file.txt");
4353 	assert(fn.extension == ".txt");
4354 
4355 	fn = FilePath("dir/");
4356 	assert(fn.directoryName == "dir/");
4357 	assert(fn.filename == "");
4358 	assert(fn.extension is null);
4359 
4360 	assert(fn.makeAbsolute(FilePath("/")).path == "/dir/");
4361 	assert(fn.makeAbsolute(FilePath("file.txt")).path == "file.txt/dir/"); // FilePaths as a base are ALWAYS treated as a directory
4362 	assert(FilePath("file.txt").makeAbsolute(fn).path == "dir/file.txt");
4363 
4364 	assert(FilePath("c:/file.txt").makeAbsolute(FilePath("d:/")).path == "c:/file.txt");
4365 	assert(FilePath("../file.txt").makeAbsolute(FilePath("d:/")).path == "d:/file.txt");
4366 
4367 	assert(FilePath("../file.txt").makeAbsolute(FilePath("d:/foo")).path == "d:/file.txt");
4368 	assert(FilePath("../file.txt").makeAbsolute(FilePath("d:/")).path == "d:/file.txt");
4369 	assert(FilePath("../file.txt").makeAbsolute(FilePath("/home/me")).path == "/home/file.txt");
4370 	assert(FilePath("../file.txt").makeAbsolute(FilePath(`\\arsd\me`)).path == `\\arsd\file.txt`);
4371 	assert(FilePath("../../file.txt").makeAbsolute(FilePath("/home/me")).path == "/file.txt");
4372 	assert(FilePath("../../../file.txt").makeAbsolute(FilePath("/home/me")).path == "/file.txt");
4373 
4374 	assert(FilePath("test/").makeAbsolute(FilePath("/home/me/")).path == "/home/me/test/");
4375 	assert(FilePath("/home/me/test/").makeAbsolute(FilePath("/home/me/test/")).path == "/home/me/test/");
4376 }
4377 
4378 version(HasFile)
4379 /++
4380 	History:
4381 		Added January 2, 2024
4382 +/
4383 FilePath getCurrentWorkingDirectory() {
4384 	version(Windows) {
4385 		wchar[256] staticBuffer;
4386 		wchar[] buffer = staticBuffer[];
4387 
4388 		try_again:
4389 		auto ret = GetCurrentDirectoryW(cast(DWORD) buffer.length, buffer.ptr);
4390 		if(ret == 0)
4391 			throw new WindowsApiException("GetCurrentDirectoryW", GetLastError());
4392 		if(ret < buffer.length) {
4393 			return FilePath(makeUtf8StringFromWindowsString(buffer[0 .. ret]));
4394 		} else {
4395 			buffer.length = ret;
4396 			goto try_again;
4397 		}
4398 	} else version(Posix) {
4399 		char[128] staticBuffer;
4400 		char[] buffer = staticBuffer[];
4401 
4402 		try_again:
4403 		auto ret = getcwd(buffer.ptr, buffer.length);
4404 		if(ret is null && errno == ERANGE && buffer.length < 4096 / 2) {
4405 			buffer.length = buffer.length * 2;
4406 			goto try_again;
4407 		} else if(ret is null) {
4408 			throw new ErrnoApiException("getcwd", errno);
4409 		}
4410 		return FilePath(stringz(ret).borrow.idup);
4411 	} else
4412 		assert(0, "Not implemented");
4413 }
4414 
4415 /++
4416 	Specialization of `string` to indicate it is a URI. You should generally use [arsd.uri.Uri] instead of this in user code.
4417 
4418 	History:
4419 		Added November 2, 2025
4420 +/
4421 struct UriString {
4422 	string uri;
4423 
4424 	alias toString this;
4425 
4426 	string toString() {
4427 		return uri;
4428 	}
4429 }
4430 
4431 /++
4432 	Shared base code for web socket client in [arsd.http2] and server in [arsd.cgi].
4433 
4434 	History:
4435 		Moved to arsd.core on November 2, 2025
4436 +/
4437 class WebSocketBase {
4438 	/* copy/paste section { */
4439 
4440 	package int readyState_;
4441 	protected ubyte[] receiveBuffer;
4442 	protected size_t receiveBufferUsedLength;
4443 
4444 	protected Config config;
4445 
4446 	enum CONNECTING = 0; /// Socket has been created. The connection is not yet open.
4447 	enum OPEN = 1; /// The connection is open and ready to communicate.
4448 	enum CLOSING = 2; /// The connection is in the process of closing.
4449 	enum CLOSED = 3; /// The connection is closed or couldn't be opened.
4450 
4451 	/++
4452 
4453 	+/
4454 	/// Group: foundational
4455 	static struct Config {
4456 		/++
4457 			These control the size of the receive buffer.
4458 
4459 			It starts at the initial size, will temporarily
4460 			balloon up to the maximum size, and will reuse
4461 			a buffer up to the likely size.
4462 
4463 			Anything larger than the maximum size will cause
4464 			the connection to be aborted and an exception thrown.
4465 			This is to protect you against a peer trying to
4466 			exhaust your memory, while keeping the user-level
4467 			processing simple.
4468 		+/
4469 		size_t initialReceiveBufferSize = 4096;
4470 		size_t likelyReceiveBufferSize = 4096; /// ditto
4471 		size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto
4472 
4473 		/++
4474 			Maximum combined size of a message.
4475 		+/
4476 		size_t maximumMessageSize = 10 * 1024 * 1024;
4477 
4478 		string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value;
4479 		string origin; /// Origin URL to send with the handshake, if desired.
4480 		string protocol; /// the protocol header, if desired.
4481 
4482 		/++
4483 			Additional headers to put in the HTTP request. These should be formatted `Name: value`, like for example:
4484 
4485 			---
4486 			Config config;
4487 			config.additionalHeaders ~= "Authorization: Bearer your_auth_token_here";
4488 			---
4489 
4490 			History:
4491 				Added February 19, 2021 (included in dub version 9.2)
4492 		+/
4493 		string[] additionalHeaders;
4494 
4495 		/++
4496 			Amount of time (in msecs) of idleness after which to send an automatic ping
4497 
4498 			Please note how this interacts with [timeoutFromInactivity] - a ping counts as activity that
4499 			keeps the socket alive.
4500 		+/
4501 		int pingFrequency = 5000;
4502 
4503 		/++
4504 			Amount of time to disconnect when there's no activity. Note that automatic pings will keep the connection alive; this timeout only occurs if there's absolutely nothing, including no responses to websocket ping frames. Since the default [pingFrequency] is only seconds, this one minute should never elapse unless the connection is actually dead.
4505 
4506 			The one thing to keep in mind is if your program is busy and doesn't check input, it might consider this a time out since there's no activity. The reason is that your program was busy rather than a connection failure, but it doesn't care. You should avoid long processing periods anyway though!
4507 
4508 			History:
4509 				Added March 31, 2021 (included in dub version 9.4)
4510 		+/
4511 		Duration timeoutFromInactivity = 1.minutes;
4512 
4513 		/++
4514 			For https connections, if this is `true`, it will fail to connect if the TLS certificate can not be
4515 			verified. Setting this to `false` will skip this check and allow the connection to continue anyway.
4516 
4517 			History:
4518 				Added April 5, 2022 (dub v10.8)
4519 
4520 				Prior to this, it always used the global (but undocumented) `defaultVerifyPeer` setting, and sometimes
4521 				even if it was true, it would skip the verification. Now, it always respects this local setting.
4522 		+/
4523 		bool verifyPeer = true;
4524 	}
4525 
4526 	/++
4527 		Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED].
4528 	+/
4529 	int readyState() {
4530 		return readyState_;
4531 	}
4532 
4533 	/++
4534 		Closes the connection, sending a graceful teardown message to the other side.
4535 		If you provide no arguments, it sends code 1000, normal closure. If you provide
4536 		a code, you should also provide a short reason string.
4537 
4538 		Params:
4539 			code = reason code.
4540 
4541 			0-999 are invalid.
4542 			1000-2999 are defined by the RFC. [https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1]
4543 				1000 - normal finish
4544 				1001 - endpoint going away
4545 				1002 - protocol error
4546 				1003 - unacceptable data received (e.g. binary message when you can't handle it)
4547 				1004 - reserved
4548 				1005 - missing status code (should not be set except by implementations)
4549 				1006 - abnormal connection closure (should only be set by implementations)
4550 				1007 - inconsistent data received (i.e. utf-8 decode error in text message)
4551 				1008 - policy violation
4552 				1009 - received message too big
4553 				1010 - client aborting due to required extension being unsupported by the server
4554 				1011 - server had unexpected failure
4555 				1015 - reserved for TLS handshake failure
4556 			3000-3999 are to be registered with IANA.
4557 			4000-4999 are private-use custom codes depending on the application. These are what you'd most commonly set here.
4558 
4559 			reason = <= 123 bytes of human-readable reason text, used for logs and debugging
4560 
4561 		History:
4562 			The default `code` was changed to 1000 on January 9, 2023. Previously it was 0,
4563 			but also ignored anyway.
4564 
4565 			On May 11, 2024, the optional arguments were changed to overloads since if you provide a code, you should also provide a reason.
4566 	+/
4567 	/// Group: foundational
4568 	void close() {
4569 		close(1000, null);
4570 	}
4571 
4572 	/// ditto
4573 	void close(int code, string reason)
4574 		//in (reason.length < 123)
4575 		in { assert(reason.length <= 123); } do
4576 	{
4577 		if(readyState_ != OPEN)
4578 			return; // it cool, we done
4579 		WebSocketFrame wss;
4580 		wss.fin = true;
4581 		wss.masked = this.isClient;
4582 		wss.opcode = WebSocketOpcode.close;
4583 		wss.data = [ubyte((code >> 8) & 0xff), ubyte(code & 0xff)] ~ cast(ubyte[]) reason.dup;
4584 		wss.send(&llsend, &getRandomByte);
4585 
4586 		readyState_ = CLOSING;
4587 
4588 		closeCalled = true;
4589 
4590 		llshutdown();
4591 	}
4592 
4593 	deprecated("If you provide a code, please also provide a reason string") void close(int code) {
4594 		close(code, null);
4595 	}
4596 
4597 
4598 	protected bool closeCalled;
4599 
4600 	/++
4601 		Sends a ping message to the server. This is done automatically by the library if you set a non-zero [Config.pingFrequency], but you can also send extra pings explicitly as well with this function.
4602 	+/
4603 	/// Group: foundational
4604 	void ping(in ubyte[] data = null) {
4605 		WebSocketFrame wss;
4606 		wss.fin = true;
4607 		wss.masked = this.isClient;
4608 		wss.opcode = WebSocketOpcode.ping;
4609 		if(data !is null) wss.data = data.dup;
4610 		wss.send(&llsend, &getRandomByte);
4611 	}
4612 
4613 	/++
4614 		Sends a pong message to the server. This is normally done automatically in response to pings.
4615 	+/
4616 	/// Group: foundational
4617 	void pong(in ubyte[] data = null) {
4618 		WebSocketFrame wss;
4619 		wss.fin = true;
4620 		wss.masked = this.isClient;
4621 		wss.opcode = WebSocketOpcode.pong;
4622 		if(data !is null) wss.data = data.dup;
4623 		wss.send(&llsend, &getRandomByte);
4624 	}
4625 
4626 	/++
4627 		Sends a text message through the websocket.
4628 	+/
4629 	/// Group: foundational
4630 	void send(in char[] textData) {
4631 		WebSocketFrame wss;
4632 		wss.fin = true;
4633 		wss.masked = this.isClient;
4634 		wss.opcode = WebSocketOpcode.text;
4635 		wss.data = cast(ubyte[]) textData.dup;
4636 		wss.send(&llsend, &getRandomByte);
4637 	}
4638 
4639 	/++
4640 		Sends a binary message through the websocket.
4641 	+/
4642 	/// Group: foundational
4643 	void send(in ubyte[] binaryData) {
4644 		WebSocketFrame wss;
4645 		wss.masked = this.isClient;
4646 		wss.fin = true;
4647 		wss.opcode = WebSocketOpcode.binary;
4648 		wss.data = cast(ubyte[]) binaryData.dup;
4649 		wss.send(&llsend, &getRandomByte);
4650 	}
4651 
4652 	/++
4653 		Waits for and returns the next complete message on the socket.
4654 
4655 		Note that the onmessage function is still called, right before
4656 		this returns.
4657 	+/
4658 	/// Group: blocking_api
4659 	public WebSocketFrame waitForNextMessage() {
4660 		do {
4661 			auto m = processOnce();
4662 			if(m.populated)
4663 				return m;
4664 		} while(lowLevelReceive());
4665 
4666 		return waitGotNothing();
4667 	}
4668 
4669 	/++
4670 		Tells if [waitForNextMessage] would block.
4671 	+/
4672 	/// Group: blocking_api
4673 	public bool waitForNextMessageWouldBlock() {
4674 		checkAgain:
4675 		if(isMessageBuffered())
4676 			return false;
4677 		if(!isDataPending())
4678 			return true;
4679 		while(isDataPending())
4680 			if(lowLevelReceive() == false)
4681 				return connectionClosedInMiddleOfMessage();
4682 		goto checkAgain;
4683 	}
4684 
4685 	/++
4686 		Is there a message in the buffer already?
4687 		If `true`, [waitForNextMessage] is guaranteed to return immediately.
4688 		If `false`, check [isDataPending] as the next step.
4689 	+/
4690 	/// Group: blocking_api
4691 	public bool isMessageBuffered() {
4692 		ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];
4693 		auto s = d;
4694 		if(d.length) {
4695 			auto orig = d;
4696 			auto m = WebSocketFrame.read(d);
4697 			// that's how it indicates that it needs more data
4698 			if(d !is orig)
4699 				return true;
4700 		}
4701 
4702 		return false;
4703 	}
4704 
4705 	protected ubyte continuingType;
4706 	protected ubyte[] continuingData;
4707 	//protected size_t continuingDataLength;
4708 
4709 	protected WebSocketFrame processOnce() {
4710 		ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];
4711 		auto s = d;
4712 		// FIXME: handle continuation frames more efficiently. it should really just reuse the receive buffer.
4713 		WebSocketFrame m;
4714 		if(d.length) {
4715 			auto orig = d;
4716 			m = WebSocketFrame.read(d);
4717 			// that's how it indicates that it needs more data
4718 			if(d is orig)
4719 				return WebSocketFrame.init;
4720 			m.unmaskInPlace();
4721 			switch(m.opcode) {
4722 				case WebSocketOpcode.continuation:
4723 					if(continuingData.length + m.data.length > config.maximumMessageSize)
4724 						throw new Exception("message size exceeded");
4725 
4726 					continuingData ~= m.data;
4727 					if(m.fin) {
4728 						if(ontextmessage)
4729 							ontextmessage(cast(char[]) continuingData);
4730 						if(onbinarymessage)
4731 							onbinarymessage(continuingData);
4732 
4733 						continuingData = null;
4734 					}
4735 				break;
4736 				case WebSocketOpcode.text:
4737 					if(m.fin) {
4738 						if(ontextmessage)
4739 							ontextmessage(m.textData);
4740 					} else {
4741 						continuingType = m.opcode;
4742 						//continuingDataLength = 0;
4743 						continuingData = null;
4744 						continuingData ~= m.data;
4745 					}
4746 				break;
4747 				case WebSocketOpcode.binary:
4748 					if(m.fin) {
4749 						if(onbinarymessage)
4750 							onbinarymessage(m.data);
4751 					} else {
4752 						continuingType = m.opcode;
4753 						//continuingDataLength = 0;
4754 						continuingData = null;
4755 						continuingData ~= m.data;
4756 					}
4757 				break;
4758 				case WebSocketOpcode.close:
4759 
4760 					//import std.stdio; writeln("closed ", cast(string) m.data);
4761 
4762 					ushort code = CloseEvent.StandardCloseCodes.noStatusCodePresent;
4763 					const(char)[] reason;
4764 
4765 					if(m.data.length >= 2) {
4766 						code = (m.data[0] << 8) | m.data[1];
4767 						reason = (cast(char[]) m.data[2 .. $]);
4768 					}
4769 
4770 					if(onclose)
4771 						onclose(CloseEvent(code, reason, true));
4772 
4773 					// if we receive one and haven't sent one back we're supposed to echo it back and close.
4774 					if(!closeCalled)
4775 						close(code, reason.idup);
4776 
4777 					readyState_ = CLOSED;
4778 
4779 					unregisterAsActiveSocket();
4780 					llclose();
4781 				break;
4782 				case WebSocketOpcode.ping:
4783 					// import std.stdio; writeln("ping received ", m.data);
4784 					pong(m.data);
4785 				break;
4786 				case WebSocketOpcode.pong:
4787 					// import std.stdio; writeln("pong received ", m.data);
4788 					// just really references it is still alive, nbd.
4789 				break;
4790 				default: // ignore though i could and perhaps should throw too
4791 			}
4792 		}
4793 
4794 		if(d.length) {
4795 			m.data = m.data.dup();
4796 		}
4797 
4798 		import core.stdc.string;
4799 		memmove(receiveBuffer.ptr, d.ptr, d.length);
4800 		receiveBufferUsedLength = d.length;
4801 
4802 		return m;
4803 	}
4804 
4805 	/++
4806 		Arguments for the close event. The `code` and `reason` are provided from the close message on the websocket, if they are present. The spec says code 1000 indicates a normal, default reason close, but reserves the code range from 3000-5000 for future definition; the 3000s can be registered with IANA and the 4000's are application private use. The `reason` should be user readable, but not displayed to the end user. `wasClean` is true if the server actually sent a close event, false if it just disconnected.
4807 
4808 		$(PITFALL
4809 			The `reason` argument references a temporary buffer and there's no guarantee it will remain valid once your callback returns. It may be freed and will very likely be overwritten. If you want to keep the reason beyond the callback, make sure you `.idup` it.
4810 		)
4811 
4812 		History:
4813 			Added March 19, 2023 (dub v11.0).
4814 	+/
4815 	static struct CloseEvent {
4816 		ushort code;
4817 		const(char)[] reason;
4818 		bool wasClean;
4819 
4820 		string extendedErrorInformationUnstable;
4821 
4822 		/++
4823 			See https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1 for details.
4824 		+/
4825 		enum StandardCloseCodes {
4826 			purposeFulfilled = 1000,
4827 			goingAway = 1001,
4828 			protocolError = 1002,
4829 			unacceptableData = 1003, // e.g. got text message when you can only handle binary
4830 			Reserved = 1004,
4831 			noStatusCodePresent = 1005, // not set by endpoint.
4832 			abnormalClosure = 1006, // not set by endpoint. closed without a Close control. FIXME: maybe keep a copy of errno around for these
4833 			inconsistentData = 1007, // e.g. utf8 validation failed
4834 			genericPolicyViolation = 1008,
4835 			messageTooBig = 1009,
4836 			clientRequiredExtensionMissing = 1010, // only the client should send this
4837 			unnexpectedCondition = 1011,
4838 			unverifiedCertificate = 1015, // not set by client
4839 		}
4840 
4841 		string toString() {
4842 			return cast(string) (arsd.core.toStringInternal(code) ~ ": " ~ reason);
4843 		}
4844 	}
4845 
4846 	/++
4847 		The `CloseEvent` you get references a temporary buffer that may be overwritten after your handler returns. If you want to keep it or the `event.reason` member, remember to `.idup` it.
4848 
4849 		History:
4850 			The `CloseEvent` was changed to a [arsd.core.FlexibleDelegate] on March 19, 2023 (dub v11.0). Before that, `onclose` was a public member of type `void delegate()`. This change means setters still work with or without the [CloseEvent] argument.
4851 
4852 			Your onclose method is now also called on abnormal terminations. Check the `wasClean` member of the `CloseEvent` to know if it came from a close frame or other cause.
4853 	+/
4854 	arsd.core.FlexibleDelegate!(void delegate(CloseEvent event)) onclose;
4855 	void delegate() onerror; ///
4856 	void delegate(in char[]) ontextmessage; ///
4857 	void delegate(in ubyte[]) onbinarymessage; ///
4858 	void delegate() onopen; ///
4859 
4860 	/++
4861 
4862 	+/
4863 	/// Group: browser_api
4864 	void onmessage(void delegate(in char[]) dg) {
4865 		ontextmessage = dg;
4866 	}
4867 
4868 	/// ditto
4869 	void onmessage(void delegate(in ubyte[]) dg) {
4870 		onbinarymessage = dg;
4871 	}
4872 
4873 	/* } end copy/paste */
4874 
4875 
4876 	// used to decide if we mask outgoing msgs
4877 	protected bool isClient;
4878 	protected abstract void llsend(ubyte[] d);
4879 	protected ubyte getRandomByte() @trusted {
4880 		// FIXME: it is just for masking but still should be less awful
4881 		__gshared ubyte seed = 0xe2;
4882 		return ++seed;
4883 	}
4884 	protected abstract void llclose();
4885 	protected abstract void llshutdown();
4886 	public abstract bool lowLevelReceive();
4887 	protected abstract bool isDataPending(Duration timeout = 0.seconds);
4888 	protected abstract void unregisterAsActiveSocket();
4889 	protected abstract WebSocketFrame waitGotNothing();
4890 	protected abstract bool connectionClosedInMiddleOfMessage();
4891 }
4892 /* copy/paste from cgi.d */
4893 public {
4894 	enum WebSocketOpcode : ubyte {
4895 		continuation = 0,
4896 		text = 1,
4897 		binary = 2,
4898 		// 3, 4, 5, 6, 7 RESERVED
4899 		close = 8,
4900 		ping = 9,
4901 		pong = 10,
4902 		// 11,12,13,14,15 RESERVED
4903 	}
4904 
4905 	public struct WebSocketFrame {
4906 		package(arsd) bool populated;
4907 		bool fin;
4908 		bool rsv1;
4909 		bool rsv2;
4910 		bool rsv3;
4911 		WebSocketOpcode opcode; // 4 bits
4912 		bool masked;
4913 		ubyte lengthIndicator; // don't set this when building one to send
4914 		ulong realLength; // don't use when sending
4915 		ubyte[4] maskingKey; // don't set this when sending
4916 		ubyte[] data;
4917 
4918 		static WebSocketFrame simpleMessage(WebSocketOpcode opcode, in void[] data) {
4919 			WebSocketFrame msg;
4920 			msg.fin = true;
4921 			msg.opcode = opcode;
4922 			msg.data = cast(ubyte[]) data.dup; // it is mutated below when masked, so need to be cautious and copy it, sigh
4923 
4924 			return msg;
4925 		}
4926 
4927 		private void send(scope void delegate(ubyte[]) llsend, scope ubyte delegate() getRandomByte) {
4928 			ubyte[64] headerScratch;
4929 			int headerScratchPos = 0;
4930 
4931 			realLength = data.length;
4932 
4933 			{
4934 				ubyte b1;
4935 				b1 |= cast(ubyte) opcode;
4936 				b1 |= rsv3 ? (1 << 4) : 0;
4937 				b1 |= rsv2 ? (1 << 5) : 0;
4938 				b1 |= rsv1 ? (1 << 6) : 0;
4939 				b1 |= fin  ? (1 << 7) : 0;
4940 
4941 				headerScratch[0] = b1;
4942 				headerScratchPos++;
4943 			}
4944 
4945 			{
4946 				headerScratchPos++; // we'll set header[1] at the end of this
4947 				auto rlc = realLength;
4948 				ubyte b2;
4949 				b2 |= masked ? (1 << 7) : 0;
4950 
4951 				assert(headerScratchPos == 2);
4952 
4953 				if(realLength > 65535) {
4954 					// use 64 bit length
4955 					b2 |= 0x7f;
4956 
4957 					// FIXME: double check endinaness
4958 					foreach(i; 0 .. 8) {
4959 						headerScratch[2 + 7 - i] = rlc & 0x0ff;
4960 						rlc >>>= 8;
4961 					}
4962 
4963 					headerScratchPos += 8;
4964 				} else if(realLength > 125) {
4965 					// use 16 bit length
4966 					b2 |= 0x7e;
4967 
4968 					// FIXME: double check endinaness
4969 					foreach(i; 0 .. 2) {
4970 						headerScratch[2 + 1 - i] = rlc & 0x0ff;
4971 						rlc >>>= 8;
4972 					}
4973 
4974 					headerScratchPos += 2;
4975 				} else {
4976 					// use 7 bit length
4977 					b2 |= realLength & 0b_0111_1111;
4978 				}
4979 
4980 				headerScratch[1] = b2;
4981 			}
4982 
4983 			//assert(!masked, "masking key not properly implemented");
4984 			if(masked) {
4985 				foreach(ref item; maskingKey)
4986 					item = getRandomByte();
4987 				headerScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[];
4988 				headerScratchPos += 4;
4989 
4990 				// we'll just mask it in place...
4991 				int keyIdx = 0;
4992 				foreach(i; 0 .. data.length) {
4993 					data[i] = data[i] ^ maskingKey[keyIdx];
4994 					if(keyIdx == 3)
4995 						keyIdx = 0;
4996 					else
4997 						keyIdx++;
4998 				}
4999 			}
5000 
5001 			//writeln("SENDING ", headerScratch[0 .. headerScratchPos], data);
5002 			llsend(headerScratch[0 .. headerScratchPos]);
5003 			if(data.length)
5004 				llsend(data);
5005 		}
5006 
5007 		static WebSocketFrame read(ref ubyte[] d) {
5008 			WebSocketFrame msg;
5009 
5010 			auto orig = d;
5011 
5012 			WebSocketFrame needsMoreData() {
5013 				d = orig;
5014 				return WebSocketFrame.init;
5015 			}
5016 
5017 			if(d.length < 2)
5018 				return needsMoreData();
5019 
5020 			ubyte b = d[0];
5021 
5022 			msg.populated = true;
5023 
5024 			msg.opcode = cast(WebSocketOpcode) (b & 0x0f);
5025 			b >>= 4;
5026 			msg.rsv3 = b & 0x01;
5027 			b >>= 1;
5028 			msg.rsv2 = b & 0x01;
5029 			b >>= 1;
5030 			msg.rsv1 = b & 0x01;
5031 			b >>= 1;
5032 			msg.fin = b & 0x01;
5033 
5034 			b = d[1];
5035 			msg.masked = (b & 0b1000_0000) ? true : false;
5036 			msg.lengthIndicator = b & 0b0111_1111;
5037 
5038 			d = d[2 .. $];
5039 
5040 			if(msg.lengthIndicator == 0x7e) {
5041 				// 16 bit length
5042 				msg.realLength = 0;
5043 
5044 				if(d.length < 2) return needsMoreData();
5045 
5046 				foreach(i; 0 .. 2) {
5047 					msg.realLength |= d[0] << ((1-i) * 8);
5048 					d = d[1 .. $];
5049 				}
5050 			} else if(msg.lengthIndicator == 0x7f) {
5051 				// 64 bit length
5052 				msg.realLength = 0;
5053 
5054 				if(d.length < 8) return needsMoreData();
5055 
5056 				foreach(i; 0 .. 8) {
5057 					msg.realLength |= ulong(d[0]) << ((7-i) * 8);
5058 					d = d[1 .. $];
5059 				}
5060 			} else {
5061 				// 7 bit length
5062 				msg.realLength = msg.lengthIndicator;
5063 			}
5064 
5065 			if(msg.masked) {
5066 
5067 				if(d.length < 4) return needsMoreData();
5068 
5069 				msg.maskingKey = d[0 .. 4];
5070 				d = d[4 .. $];
5071 			}
5072 
5073 			if(msg.realLength > d.length) {
5074 				return needsMoreData();
5075 			}
5076 
5077 			msg.data = d[0 .. cast(size_t) msg.realLength];
5078 			d = d[cast(size_t) msg.realLength .. $];
5079 
5080 			return msg;
5081 		}
5082 
5083 		void unmaskInPlace() {
5084 			if(this.masked) {
5085 				int keyIdx = 0;
5086 				foreach(i; 0 .. this.data.length) {
5087 					this.data[i] = this.data[i] ^ this.maskingKey[keyIdx];
5088 					if(keyIdx == 3)
5089 						keyIdx = 0;
5090 					else
5091 						keyIdx++;
5092 				}
5093 			}
5094 		}
5095 
5096 		char[] textData() {
5097 			return cast(char[]) data;
5098 		}
5099 	}
5100 }
5101 
5102 /+
5103 struct FilePathGeneric {
5104 
5105 }
5106 
5107 struct FilePathWin32 {
5108 
5109 }
5110 
5111 struct FilePathPosix {
5112 
5113 }
5114 
5115 struct FilePathWindowsUnc {
5116 
5117 }
5118 
5119 version(Windows)
5120 	alias FilePath = FilePathWin32;
5121 else
5122 	alias FilePath = FilePathPosix;
5123 +/
5124 
5125 
5126 /++
5127 	Represents a generic async, waitable request.
5128 +/
5129 class AsyncOperationRequest {
5130 	/++
5131 		Actually issues the request, starting the operation.
5132 	+/
5133 	abstract void start();
5134 	/++
5135 		Cancels the request. This will cause `isComplete` to return true once the cancellation has been processed, but [AsyncOperationResponse.wasSuccessful] will return `false` (unless it completed before the cancellation was processed, in which case it is still allowed to finish successfully).
5136 
5137 		After cancelling a request, you should still wait for it to complete to ensure that the task has actually released its resources before doing anything else on it.
5138 
5139 		Once a cancellation request has been sent, it cannot be undone.
5140 	+/
5141 	abstract void cancel();
5142 
5143 	/++
5144 		Returns `true` if the operation has been completed. It may be completed successfully, cancelled, or have errored out - to check this, call [waitForCompletion] and check the members on the response object.
5145 	+/
5146 	abstract bool isComplete();
5147 	/++
5148 		Waits until the request has completed - successfully or otherwise - and returns the response object. It will run an ad-hoc event loop that may call other callbacks while waiting.
5149 
5150 		The response object may be embedded in the request object - do not reuse the request until you are finished with the response and do not keep the response around longer than you keep the request.
5151 
5152 
5153 		Note to implementers: all subclasses should override this and return their specific response object. You can use the top-level `waitForFirstToCompleteByIndex` function with a single-element static array to help with the implementation.
5154 	+/
5155 	abstract AsyncOperationResponse waitForCompletion();
5156 
5157 	/++
5158 
5159 	+/
5160 	// abstract void repeat();
5161 }
5162 
5163 /++
5164 
5165 +/
5166 interface AsyncOperationResponse {
5167 	/++
5168 		Returns true if the request completed successfully, finishing what it was supposed to.
5169 
5170 		Should be set to `false` if the request was cancelled before completing or encountered an error.
5171 	+/
5172 	bool wasSuccessful();
5173 }
5174 
5175 /++
5176 	It returns the $(I request) so you can identify it more easily. `request.waitForCompletion()` is guaranteed to return the response without any actual wait, since it is already complete when this function returns.
5177 
5178 	Please note that "completion" is not necessary successful completion; a request being cancelled or encountering an error also counts as it being completed.
5179 
5180 	The `waitForFirstToCompleteByIndex` version instead returns the index of the array entry that completed first.
5181 
5182 	It is your responsibility to remove the completed request from the array before calling the function again, since any request already completed will always be immediately returned.
5183 
5184 	You might prefer using [asTheyComplete], which will give each request as it completes and loop over until all of them are complete.
5185 
5186 	Returns:
5187 		`null` or `requests.length` if none completed before returning.
5188 +/
5189 AsyncOperationRequest waitForFirstToComplete(AsyncOperationRequest[] requests...) {
5190 	auto idx = waitForFirstToCompleteByIndex(requests);
5191 	if(idx == requests.length)
5192 		return null;
5193 	return requests[idx];
5194 }
5195 /// ditto
5196 size_t waitForFirstToCompleteByIndex(AsyncOperationRequest[] requests...) {
5197 	size_t helper() {
5198 		foreach(idx, request; requests)
5199 			if(request.isComplete())
5200 				return idx;
5201 		return requests.length;
5202 	}
5203 
5204 	auto idx = helper();
5205 	// if one is already done, return it
5206 	if(idx != requests.length)
5207 		return idx;
5208 
5209 	// otherwise, run the ad-hoc event loop until one is
5210 	// FIXME: what if we are inside a fiber?
5211 	auto el = getThisThreadEventLoop();
5212 	el.run(() => (idx = helper()) != requests.length);
5213 
5214 	return idx;
5215 }
5216 
5217 /++
5218 	Waits for all the `requests` to complete, giving each one through the range interface as it completes.
5219 
5220 	This meant to be used in a foreach loop.
5221 
5222 	The `requests` array and its contents must remain valid for the lifetime of the returned range. Its contents may be shuffled as the requests complete (the implementation works through an unstable sort+remove).
5223 +/
5224 AsTheyCompleteRange asTheyComplete(AsyncOperationRequest[] requests...) {
5225 	return AsTheyCompleteRange(requests);
5226 }
5227 /// ditto
5228 struct AsTheyCompleteRange {
5229 	AsyncOperationRequest[] requests;
5230 
5231 	this(AsyncOperationRequest[] requests) {
5232 		this.requests = requests;
5233 
5234 		if(requests.length == 0)
5235 			return;
5236 
5237 		// wait for first one to complete, then move it to the front of the array
5238 		moveFirstCompleteToFront();
5239 	}
5240 
5241 	private void moveFirstCompleteToFront() {
5242 		auto idx = waitForFirstToCompleteByIndex(requests);
5243 
5244 		auto tmp = requests[0];
5245 		requests[0] = requests[idx];
5246 		requests[idx] = tmp;
5247 	}
5248 
5249 	bool empty() {
5250 		return requests.length == 0;
5251 	}
5252 
5253 	void popFront() {
5254 		assert(!empty);
5255 		/+
5256 			this needs to
5257 			1) remove the front of the array as being already processed (unless it is the initial priming call)
5258 			2) wait for one of them to complete
5259 			3) move the complete one to the front of the array
5260 		+/
5261 
5262 		requests[0] = requests[$-1];
5263 		requests = requests[0 .. $-1];
5264 
5265 		if(requests.length)
5266 			moveFirstCompleteToFront();
5267 	}
5268 
5269 	AsyncOperationRequest front() {
5270 		return requests[0];
5271 	}
5272 }
5273 
5274 version(Windows) {
5275 	alias NativeFileHandle = HANDLE; ///
5276 	alias NativeSocketHandle = SOCKET; ///
5277 	alias NativePipeHandle = HANDLE; ///
5278 } else version(Posix) {
5279 	alias NativeFileHandle = int; ///
5280 	alias NativeSocketHandle = int; ///
5281 	alias NativePipeHandle = int; ///
5282 }
5283 
5284 /++
5285 	An `AbstractFile` represents a file handle on the operating system level. You cannot do much with it.
5286 +/
5287 version(HasFile) class AbstractFile {
5288 	private {
5289 		NativeFileHandle handle;
5290 	}
5291 
5292 	/++
5293 	+/
5294 	enum OpenMode {
5295 		readOnly, /// C's "r", the file is read
5296 		writeWithTruncation, /// C's "w", the file is blanked upon opening so it only holds what you write
5297 		appendOnly, /// C's "a", writes will always be appended to the file
5298 		readAndWrite /// C's "r+", writes will overwrite existing parts of the file based on where you seek (default is at the beginning)
5299 	}
5300 
5301 	/++
5302 	+/
5303 	enum RequirePreexisting {
5304 		no,
5305 		yes
5306 	}
5307 
5308 	/+
5309 	enum SpecialFlags {
5310 		randomAccessExpected, /// FILE_FLAG_SEQUENTIAL_SCAN is turned off and posix_fadvise(POSIX_FADV_SEQUENTIAL)
5311 		skipCache, /// O_DSYNC, FILE_FLAG_NO_BUFFERING and maybe WRITE_THROUGH. note that metadata still goes through the cache, FlushFileBuffers and fsync can still do those
5312 		temporary, /// FILE_ATTRIBUTE_TEMPORARY on Windows, idk how to specify on linux. also FILE_FLAG_DELETE_ON_CLOSE can be combined to make a (almost) all memory file. kinda like a private anonymous mmap i believe.
5313 		deleteWhenClosed, /// Windows has a flag for this but idk if it is of any real use
5314 		async, /// open it in overlapped mode, all reads and writes must then provide an offset. Only implemented on Windows
5315 	}
5316 	+/
5317 
5318 	/++
5319 
5320 	+/
5321 	protected this(bool async, FilePath filename, OpenMode mode = OpenMode.readOnly, RequirePreexisting require = RequirePreexisting.no, uint specialFlags = 0) {
5322 		version(Windows) {
5323 			DWORD access;
5324 			DWORD creation;
5325 
5326 			final switch(mode) {
5327 				case OpenMode.readOnly:
5328 					access = GENERIC_READ;
5329 					creation = OPEN_EXISTING;
5330 				break;
5331 				case OpenMode.writeWithTruncation:
5332 					access = GENERIC_WRITE;
5333 
5334 					final switch(require) {
5335 						case RequirePreexisting.no:
5336 							creation = CREATE_ALWAYS;
5337 						break;
5338 						case RequirePreexisting.yes:
5339 							creation = TRUNCATE_EXISTING;
5340 						break;
5341 					}
5342 				break;
5343 				case OpenMode.appendOnly:
5344 					access = FILE_APPEND_DATA;
5345 
5346 					final switch(require) {
5347 						case RequirePreexisting.no:
5348 							creation = OPEN_ALWAYS;
5349 						break;
5350 						case RequirePreexisting.yes:
5351 							creation = OPEN_EXISTING;
5352 						break;
5353 					}
5354 				break;
5355 				case OpenMode.readAndWrite:
5356 					access = GENERIC_READ | GENERIC_WRITE;
5357 
5358 					final switch(require) {
5359 						case RequirePreexisting.no:
5360 							creation = CREATE_NEW;
5361 						break;
5362 						case RequirePreexisting.yes:
5363 							creation = OPEN_EXISTING;
5364 						break;
5365 					}
5366 				break;
5367 			}
5368 
5369 			WCharzBuffer wname = WCharzBuffer(filename.path);
5370 
5371 			auto handle = CreateFileW(
5372 				wname.ptr,
5373 				access,
5374 				FILE_SHARE_READ,
5375 				null,
5376 				creation,
5377 				FILE_ATTRIBUTE_NORMAL | (async ? FILE_FLAG_OVERLAPPED : 0),
5378 				null
5379 			);
5380 
5381 			if(handle == INVALID_HANDLE_VALUE) {
5382 				// FIXME: throw the filename and other params here too
5383 				SavedArgument[3] args;
5384 				args[0] = SavedArgument("filename", LimitedVariant(filename.path));
5385 				args[1] = SavedArgument("access", LimitedVariant(access, 2));
5386 				args[2] = SavedArgument("requirePreexisting", LimitedVariant(require == RequirePreexisting.yes));
5387 				throw new WindowsApiException("CreateFileW", GetLastError(), args[]);
5388 			}
5389 
5390 			this.handle = handle;
5391 		} else version(Posix) {
5392 			import core.sys.posix.unistd;
5393 			import core.sys.posix.fcntl;
5394 
5395 			CharzBuffer namez = CharzBuffer(filename.path);
5396 			int flags;
5397 
5398 			// FIXME does mac not have cloexec for real or is this just a druntime problem?????
5399 			version(Arsd_core_has_cloexec) {
5400 				flags = O_CLOEXEC;
5401 			} else {
5402 				scope(success)
5403 					setCloExec(this.handle);
5404 			}
5405 
5406 			if(async)
5407 				flags |= O_NONBLOCK;
5408 
5409 			final switch(mode) {
5410 				case OpenMode.readOnly:
5411 					flags |= O_RDONLY;
5412 				break;
5413 				case OpenMode.writeWithTruncation:
5414 					flags |= O_WRONLY | O_TRUNC;
5415 
5416 					final switch(require) {
5417 						case RequirePreexisting.no:
5418 							flags |= O_CREAT;
5419 						break;
5420 						case RequirePreexisting.yes:
5421 						break;
5422 					}
5423 				break;
5424 				case OpenMode.appendOnly:
5425 					flags |= O_WRONLY | O_APPEND;
5426 
5427 					final switch(require) {
5428 						case RequirePreexisting.no:
5429 							flags |= O_CREAT;
5430 						break;
5431 						case RequirePreexisting.yes:
5432 						break;
5433 					}
5434 				break;
5435 				case OpenMode.readAndWrite:
5436 					flags |= O_RDWR;
5437 
5438 					final switch(require) {
5439 						case RequirePreexisting.no:
5440 							flags |= O_CREAT;
5441 						break;
5442 						case RequirePreexisting.yes:
5443 						break;
5444 					}
5445 				break;
5446 			}
5447 
5448 			auto perms = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
5449 			int fd = open(namez.ptr, flags, perms);
5450 			if(fd == -1) {
5451 				SavedArgument[3] args;
5452 				args[0] = SavedArgument("filename", LimitedVariant(filename.path));
5453 				args[1] = SavedArgument("flags", LimitedVariant(flags, 2));
5454 				args[2] = SavedArgument("perms", LimitedVariant(perms, 8));
5455 				throw new ErrnoApiException("open", errno, args[]);
5456 			}
5457 
5458 			this.handle = fd;
5459 		}
5460 	}
5461 
5462 	/++
5463 
5464 	+/
5465 	private this(NativeFileHandle handleToWrap) {
5466 		this.handle = handleToWrap;
5467 	}
5468 
5469 	// only available on some types of file
5470 	long size() { return 0; }
5471 
5472 	// note that there is no fsync thing, instead use the special flag.
5473 
5474 	/++
5475 
5476 	+/
5477 	void close() {
5478 		version(Windows) {
5479 			Win32Enforce!CloseHandle(handle);
5480 			handle = null;
5481 		} else version(Posix) {
5482 			import unix = core.sys.posix.unistd;
5483 			import core.sys.posix.fcntl;
5484 
5485 			ErrnoEnforce!(unix.close)(handle);
5486 			handle = -1;
5487 		}
5488 	}
5489 
5490 	NativeFileHandle nativeHandle() {
5491 		return this.handle;
5492 	}
5493 }
5494 
5495 /++
5496 
5497 +/
5498 version(HasFile) class File : AbstractFile {
5499 
5500 	/++
5501 		Opens a file in synchronous access mode.
5502 
5503 		The permission mask is on used on posix systems FIXME: implement it
5504 	+/
5505 	this(FilePath filename, OpenMode mode = OpenMode.readOnly, RequirePreexisting require = RequirePreexisting.no, uint specialFlags = 0, uint permMask = 0) {
5506 		super(false, filename, mode, require, specialFlags);
5507 	}
5508 
5509 	this(NativeFileHandle wrapWithoutOtherwiseChanging) {
5510 		super(wrapWithoutOtherwiseChanging);
5511 	}
5512 
5513 	/++
5514 
5515 	+/
5516 	ubyte[] read(scope ubyte[] buffer) {
5517 		return null;
5518 	}
5519 
5520 	/++
5521 
5522 	+/
5523 	void write(in void[] buffer) {
5524 	}
5525 
5526 	enum Seek {
5527 		current,
5528 		fromBeginning,
5529 		fromEnd
5530 	}
5531 
5532 	// Seeking/telling/sizing is not permitted when appending and some files don't support it
5533 	// also not permitted in async mode
5534 	void seek(long where, Seek fromWhence) {}
5535 	long tell() { return 0; }
5536 }
5537 
5538 /++
5539 	 Only one operation can be pending at any time in the current implementation.
5540 +/
5541 version(HasFile) class AsyncFile : AbstractFile {
5542 	/++
5543 		Opens a file in asynchronous access mode.
5544 	+/
5545 	this(FilePath filename, OpenMode mode = OpenMode.readOnly, RequirePreexisting require = RequirePreexisting.no, uint specialFlags = 0, uint permissionMask = 0) {
5546 		// FIXME: implement permissionMask
5547 		super(true, filename, mode, require, specialFlags);
5548 	}
5549 
5550 	package(arsd) this(NativeFileHandle adoptPreSetup) {
5551 		super(adoptPreSetup);
5552 	}
5553 
5554 	///
5555 	AsyncReadRequest read(ubyte[] buffer, long offset = 0) {
5556 		return new AsyncReadRequest(this, buffer, offset);
5557 	}
5558 
5559 	///
5560 	AsyncWriteRequest write(const(void)[] buffer, long offset = 0) {
5561 		return new AsyncWriteRequest(this, cast(ubyte[]) buffer, offset);
5562 	}
5563 
5564 }
5565 else class AsyncFile {
5566 	package(arsd) this(NativeFileHandle adoptPreSetup) {}
5567 }
5568 
5569 /++
5570 	Reads or writes a file in one call. It might internally yield, but is generally blocking if it returns values. The callback ones depend on the implementation.
5571 
5572 	Tip: prefer the callback ones. If settings where async is possible, it will do async, and if not, it will sync.
5573 
5574 	NOT FULLY IMPLEMENTED
5575 +/
5576 void writeFile(string filename, const(void)[] contents) {
5577 	// FIXME: stop using the C lib and start error checking
5578 	import core.stdc.stdio;
5579 	CharzBuffer fn = filename;
5580 	auto file = fopen(fn.ptr, "wb");
5581 	if(file is null)
5582 		throw new ErrnoApiException("fopen", errno, [SavedArgument("filename", LimitedVariant(filename))]);
5583 	fwrite(contents.ptr, 1, contents.length, file);
5584 	fclose(file);
5585 }
5586 
5587 /// ditto
5588 const(ubyte[]) readBinaryFile(string filename) {
5589 	// FIXME: stop using the C lib and check for more errors
5590 
5591 	import core.stdc.stdio;
5592 	CharzBuffer fn = filename;
5593 	auto file = fopen(fn.ptr, "rb");
5594 	if(file is null)
5595 		throw new ErrnoApiException("fopen", errno, [SavedArgument("filename", LimitedVariant(filename))]);
5596 	ubyte[] buffer = new ubyte[](64 * 1024);
5597 	ubyte[] contents;
5598 
5599 	while(true) {
5600 		auto ret = fread(buffer.ptr, 1, buffer.length, file);
5601 		if(ret < buffer.length) {
5602 			if(contents is null)
5603 				contents = buffer[0 .. ret];
5604 			else
5605 				contents ~= buffer[0 .. ret];
5606 			break;
5607 		} else {
5608 			contents ~= buffer[0 .. ret];
5609 		}
5610 	}
5611 	fclose(file);
5612 
5613 	return contents;
5614 }
5615 
5616 /// ditto
5617 string readTextFile(string filename, string fileEncoding = null) {
5618 	return cast(string) readBinaryFile(filename);
5619 }
5620 
5621 /+
5622 private Class recycleObject(Class, Args...)(Class objectToRecycle, Args args) {
5623 	if(objectToRecycle is null)
5624 		return new Class(args);
5625 	// destroy nulls out the vtable which is the first thing in the object
5626 	// so if it hasn't already been destroyed, we'll do it here
5627 	if((*cast(void**) objectToRecycle) !is null) {
5628 		assert(typeid(objectToRecycle) is typeid(Class)); // to make sure we're actually recycling the right kind of object
5629 		.destroy(objectToRecycle);
5630 	}
5631 
5632 	// then go ahead and reinitialize it
5633 	ubyte[] rawData = (cast(ubyte*) cast(void*) objectToRecycle)[0 .. __traits(classInstanceSize, Class)];
5634 	rawData[] = (cast(ubyte[]) typeid(Class).initializer)[];
5635 
5636 	objectToRecycle.__ctor(args);
5637 
5638 	return objectToRecycle;
5639 }
5640 +/
5641 
5642 /+
5643 /++
5644 	Preallocates a class object without initializing it.
5645 
5646 	This is suitable *only* for passing to one of the functions in here that takes a preallocated object for recycling.
5647 +/
5648 Class preallocate(Class)() {
5649 	import core.memory;
5650 	// FIXME: can i pass NO_SCAN here?
5651 	return cast(Class) GC.calloc(__traits(classInstanceSize, Class), 0, typeid(Class));
5652 }
5653 
5654 OwnedClass!Class preallocateOnStack(Class)() {
5655 
5656 }
5657 +/
5658 
5659 // thanks for a random person on stack overflow for this function
5660 version(Windows)
5661 BOOL MyCreatePipeEx(
5662 	PHANDLE lpReadPipe,
5663 	PHANDLE lpWritePipe,
5664 	LPSECURITY_ATTRIBUTES lpPipeAttributes,
5665 	DWORD nSize,
5666 	DWORD dwReadMode,
5667 	DWORD dwWriteMode
5668 )
5669 {
5670 	HANDLE ReadPipeHandle, WritePipeHandle;
5671 	DWORD dwError;
5672 	CHAR[MAX_PATH] PipeNameBuffer;
5673 
5674 	if (nSize == 0) {
5675 		nSize = 4096;
5676 	}
5677 
5678 	// FIXME: should be atomic op and gshared
5679 	static shared(int) PipeSerialNumber = 0;
5680 
5681 	import core.stdc.string;
5682 	import core.stdc.stdio;
5683 
5684 	sprintf(PipeNameBuffer.ptr,
5685 		"\\\\.\\Pipe\\ArsdCoreAnonymousPipe.%08x.%08x".ptr,
5686 		GetCurrentProcessId(),
5687 		atomicOp!"+="(PipeSerialNumber, 1)
5688 	);
5689 
5690 	ReadPipeHandle = CreateNamedPipeA(
5691 		PipeNameBuffer.ptr,
5692 		1/*PIPE_ACCESS_INBOUND*/ | dwReadMode,
5693 		0/*PIPE_TYPE_BYTE*/ | 0/*PIPE_WAIT*/,
5694 		1,             // Number of pipes
5695 		nSize,         // Out buffer size
5696 		nSize,         // In buffer size
5697 		120 * 1000,    // Timeout in ms
5698 		lpPipeAttributes
5699 	);
5700 
5701 	if (! ReadPipeHandle) {
5702 		return FALSE;
5703 	}
5704 
5705 	WritePipeHandle = CreateFileA(
5706 		PipeNameBuffer.ptr,
5707 		GENERIC_WRITE,
5708 		0,                         // No sharing
5709 		lpPipeAttributes,
5710 		OPEN_EXISTING,
5711 		FILE_ATTRIBUTE_NORMAL | dwWriteMode,
5712 		null                       // Template file
5713 	);
5714 
5715 	if (INVALID_HANDLE_VALUE == WritePipeHandle) {
5716 		dwError = GetLastError();
5717 		CloseHandle( ReadPipeHandle );
5718 		SetLastError(dwError);
5719 		return FALSE;
5720 	}
5721 
5722 	*lpReadPipe = ReadPipeHandle;
5723 	*lpWritePipe = WritePipeHandle;
5724 	return( TRUE );
5725 }
5726 
5727 
5728 
5729 /+
5730 
5731 	// this is probably useless.
5732 
5733 /++
5734 	Creates a pair of anonymous pipes ready for async operations.
5735 
5736 	You can pass some preallocated objects to recycle if you like.
5737 +/
5738 AsyncAnonymousPipe[2] anonymousPipePair(AsyncAnonymousPipe[2] preallocatedObjects = [null, null], bool inheritable = false) {
5739 	version(Posix) {
5740 		int[2] fds;
5741 		auto ret = pipe(fds);
5742 
5743 		if(ret == -1)
5744 			throw new SystemApiException("pipe", errno);
5745 
5746 		// FIXME: do we want them inheritable? and do we want both sides to be async?
5747 		if(!inheritable) {
5748 			setCloExec(fds[0]);
5749 			setCloExec(fds[1]);
5750 		}
5751 		// if it is inherited, do we actually want it non-blocking?
5752 		makeNonBlocking(fds[0]);
5753 		makeNonBlocking(fds[1]);
5754 
5755 		return [
5756 			recycleObject(preallocatedObjects[0], fds[0]),
5757 			recycleObject(preallocatedObjects[1], fds[1]),
5758 		];
5759 	} else version(Windows) {
5760 		HANDLE rp, wp;
5761 		// FIXME: do we want them inheritable? and do we want both sides to be async?
5762 		if(!MyCreatePipeEx(&rp, &wp, null, 0, FILE_FLAG_OVERLAPPED, FILE_FLAG_OVERLAPPED))
5763 			throw new SystemApiException("MyCreatePipeEx", GetLastError());
5764 		return [
5765 			recycleObject(preallocatedObjects[0], rp),
5766 			recycleObject(preallocatedObjects[1], wp),
5767 		];
5768 	} else throw ArsdException!"NotYetImplemented"();
5769 }
5770 	// on posix, just do pipe() w/ non block
5771 	// on windows, do an overlapped named pipe server, connect, stop listening, return pair.
5772 +/
5773 
5774 /+
5775 class NamedPipe : AsyncFile {
5776 
5777 }
5778 +/
5779 
5780 /++
5781 	A named pipe ready to accept connections.
5782 
5783 	A Windows named pipe is an IPC mechanism usable on local machines or across a Windows network.
5784 +/
5785 version(Windows)
5786 class NamedPipeServer {
5787 	// unix domain socket or windows named pipe
5788 
5789 	// Promise!AsyncAnonymousPipe connect;
5790 	// Promise!AsyncAnonymousPipe accept;
5791 
5792 	// when a new connection arrives, it calls your callback
5793 	// can be on a specific thread or on any thread
5794 }
5795 
5796 private version(Windows) extern(Windows) {
5797 	const(char)* inet_ntop(int, const void*, char*, socklen_t);
5798 }
5799 
5800 /++
5801 	Some functions that return arrays allow you to provide your own buffer. These are indicated in the type system as `UserProvidedBuffer!Type`, and you get to decide what you want to happen if the buffer is too small via the [OnOutOfSpace] parameter.
5802 
5803 	These are usually optional, since an empty user provided buffer with the default policy of reallocate will also work fine for whatever needs to be returned, thanks to the garbage collector taking care of it for you.
5804 
5805 	The API inside `UserProvidedBuffer` is all private to the arsd library implementation; your job is just to provide the buffer to it with [provideBuffer] or a constructor call and decide on your on-out-of-space policy.
5806 
5807 	$(TIP
5808 		To properly size a buffer, I suggest looking at what covers about 80% of cases. Trying to cover everything often leads to wasted buffer space, and if you use a reallocate policy it can cover the rest. You might be surprised how far just two elements can go!
5809 	)
5810 
5811 	History:
5812 		Added August 4, 2023 (dub v11.0)
5813 +/
5814 struct UserProvidedBuffer(T) {
5815 	private T[] buffer;
5816 	private int actualLength;
5817 	private OnOutOfSpace policy;
5818 
5819 	/++
5820 
5821 	+/
5822 	public this(scope T[] buffer, OnOutOfSpace policy = OnOutOfSpace.reallocate) {
5823 		this.buffer = buffer;
5824 		this.policy = policy;
5825 	}
5826 
5827 	package(arsd) bool append(T item) {
5828 		if(actualLength < buffer.length) {
5829 			buffer[actualLength++] = item;
5830 			return true;
5831 		} else final switch(policy) {
5832 			case OnOutOfSpace.discard:
5833 				return false;
5834 			case OnOutOfSpace.exception:
5835 				throw ArsdException!"Buffer out of space"(buffer.length, actualLength);
5836 			case OnOutOfSpace.reallocate:
5837 				buffer ~= item;
5838 				actualLength++;
5839 				return true;
5840 		}
5841 	}
5842 
5843 	package(arsd) T[] slice() {
5844 		return buffer[0 .. actualLength];
5845 	}
5846 }
5847 
5848 /// ditto
5849 UserProvidedBuffer!T provideBuffer(T)(scope T[] buffer, OnOutOfSpace policy = OnOutOfSpace.reallocate) {
5850 	return UserProvidedBuffer!T(buffer, policy);
5851 }
5852 
5853 /++
5854 	Possible policies for [UserProvidedBuffer]s that run out of space.
5855 +/
5856 enum OnOutOfSpace {
5857 	reallocate, /// reallocate the buffer with the GC to make room
5858 	discard, /// discard all contents that do not fit in your provided buffer
5859 	exception, /// throw an exception if there is data that would not fit in your provided buffer
5860 }
5861 
5862 
5863 
5864 /+
5865 	The GC can be called from any thread, and a lot of cleanup must be done
5866 	on the gui thread. Since the GC can interrupt any locks - including being
5867 	triggered inside a critical section - it is vital to avoid deadlocks to get
5868 	these functions called from the right place.
5869 
5870 	If the buffer overflows, things are going to get leaked. I'm kinda ok with that
5871 	right now.
5872 
5873 	The cleanup function is run when the event loop gets around to it, which is just
5874 	whenever there's something there after it has been woken up for other work. It does
5875 	NOT wake up the loop itself - can't risk doing that from inside the GC in another thread.
5876 	(Well actually it might be ok but i don't wanna mess with it right now.)
5877 +/
5878 package(arsd) struct CleanupQueue {
5879 	import core.stdc.stdlib;
5880 
5881 	void queue(alias func, T...)(T args) {
5882 		static struct Args {
5883 			T args;
5884 		}
5885 		static struct RealJob {
5886 			Job j;
5887 			Args a;
5888 		}
5889 		static void call(Job* data) {
5890 			auto rj = cast(RealJob*) data;
5891 			func(rj.a.args);
5892 		}
5893 
5894 		RealJob* thing = cast(RealJob*) malloc(RealJob.sizeof);
5895 		thing.j.call = &call;
5896 		thing.a.args = args;
5897 
5898 		buffer[tail++] = cast(Job*) thing;
5899 
5900 		// FIXME: set overflowed
5901 	}
5902 
5903 	void process() {
5904 		const tail = this.tail;
5905 
5906 		while(tail != head) {
5907 			Job* job = cast(Job*) buffer[head++];
5908 			job.call(job);
5909 			free(job);
5910 		}
5911 
5912 		if(overflowed)
5913 			throw new object.Exception("cleanup overflowed");
5914 	}
5915 
5916 	private:
5917 
5918 	ubyte tail; // must ONLY be written by queue
5919 	ubyte head; // must ONLY be written by process
5920 	bool overflowed;
5921 
5922 	static struct Job {
5923 		void function(Job*) call;
5924 	}
5925 
5926 	void*[256] buffer;
5927 }
5928 package(arsd) __gshared CleanupQueue cleanupQueue;
5929 
5930 
5931 
5932 
5933 /++
5934 	A timer that will trigger your function on a given interval.
5935 
5936 
5937 	You create a timer with an interval and a callback. It will continue
5938 	to fire on the interval until it is destroyed.
5939 
5940 	---
5941 	auto timer = new Timer(50, { it happened!; });
5942 	timer.destroy();
5943 	---
5944 
5945 	Timers can only be expected to fire when the event loop is running and only
5946 	once per iteration through the event loop.
5947 
5948 	History:
5949 		Prior to December 9, 2020, a timer pulse set too high with a handler too
5950 		slow could lock up the event loop. It now guarantees other things will
5951 		get a chance to run between timer calls, even if that means not keeping up
5952 		with the requested interval.
5953 
5954 		Originally part of arsd.simpledisplay, this code was integrated into
5955 		arsd.core on May 26, 2024 (committed on June 10).
5956 +/
5957 version(HasTimer)
5958 class Timer {
5959 	// FIXME: absolute time vs relative time
5960 	// FIXME: real time?
5961 
5962 	// FIXME: I might add overloads for ones that take a count of
5963 	// how many elapsed since last time (on Windows, it will divide
5964 	// the ticks thing given, on Linux it is just available) and
5965 	// maybe one that takes an instance of the Timer itself too
5966 
5967 
5968 	/++
5969 		Creates an initialized, but unarmed timer. You must call other methods later.
5970 	+/
5971 	this(bool actuallyInitialize = true) {
5972 		if(actuallyInitialize)
5973 			initialize();
5974 	}
5975 
5976 	private void initialize() {
5977 		version(Windows) {
5978 			handle = CreateWaitableTimer(null, false, null);
5979 			if(handle is null)
5980 				throw new WindowsApiException("CreateWaitableTimer", GetLastError());
5981 			cbh = new CallbackHelper(&trigger);
5982 		} else version(Emscripten) {
5983 			assert(0);
5984 		} else version(linux) {
5985 			import core.sys.linux.timerfd;
5986 
5987 			fd = timerfd_create(CLOCK_MONOTONIC, 0);
5988 			if(fd == -1)
5989 				throw new Exception("timer create failed");
5990 
5991 			auto el = getThisThreadEventLoop(EventLoopType.Ui);
5992 			unregisterToken = el.addCallbackOnFdReadable(fd, new CallbackHelper(&trigger));
5993 		} else version(Arsd_core_kqueue) {
5994 			this.ident = ++identTicker;
5995 		} else throw new NotYetImplementedException();
5996 		// FIXME: freebsd 12 has timer_fd and netbsd 10 too
5997 	}
5998 
5999 	/++
6000 	+/
6001 	void setPulseCallback(void delegate() onPulse) {
6002 		assert(onPulse !is null);
6003 		this.onPulse = onPulse;
6004 	}
6005 
6006 	/++
6007 	+/
6008 	void changeTime(int intervalInMilliseconds, bool repeats) {
6009 		this.intervalInMilliseconds = intervalInMilliseconds;
6010 		this.repeats = repeats;
6011 		changeTimeInternal(intervalInMilliseconds, repeats);
6012 	}
6013 
6014 	private void changeTimeInternal(int intervalInMilliseconds, bool repeats) {
6015 		version(Windows)
6016 		{
6017 			LARGE_INTEGER initialTime;
6018 			initialTime.QuadPart = -intervalInMilliseconds * 10000000L / 1000; // Windows wants hnsecs, we have msecs
6019 			if(!SetWaitableTimer(handle, &initialTime, repeats ? intervalInMilliseconds : 0, &timerCallback, cast(void*) cbh, false))
6020 				throw new WindowsApiException("SetWaitableTimer", GetLastError());
6021 		} else version(Emscripten) {
6022 			assert(0);
6023 		} else version(linux) {
6024 			import core.sys.linux.timerfd;
6025 
6026 			itimerspec value = makeItimerspec(intervalInMilliseconds, repeats);
6027 			if(timerfd_settime(fd, 0, &value, null) == -1) {
6028 				throw new ErrnoApiException("couldn't change pulse timer", errno);
6029 			}
6030 		} else version(Arsd_core_kqueue) {
6031 			// FIXME
6032 
6033 			auto el = cast(CoreEventLoopImplementation) getThisThreadEventLoop();
6034 
6035 			kevent_t ev;
6036 
6037 			cbh = new CallbackHelper(&trigger);
6038 
6039 			EV_SET(&ev, this.ident, EVFILT_TIMER, EV_ADD | EV_ENABLE | EV_CLEAR | (repeats ? 0 : EV_ONESHOT), NOTE_USECONDS, 1000 * intervalInMilliseconds, cast(void*) cbh);
6040 			ErrnoEnforce!kevent(el.kqueuefd, &ev, 1, null, 0, null);
6041 		} else {
6042 			throw new NotYetImplementedException();
6043 		}
6044 		// FIXME: freebsd 12 has timer_fd and netbsd 10 too
6045 	}
6046 
6047 	/++
6048 	+/
6049 	void pause() {
6050 		// FIXME this kinda makes little sense tbh
6051 		// when it restarts, it won't be on the same rhythm as it was at first...
6052 		changeTimeInternal(0, false);
6053 	}
6054 
6055 	/++
6056 	+/
6057 	void unpause() {
6058 		changeTimeInternal(this.intervalInMilliseconds, this.repeats);
6059 	}
6060 
6061 	/++
6062 	+/
6063 	void cancel() {
6064 		version(Windows)
6065 			CancelWaitableTimer(handle);
6066 		else
6067 			changeTime(0, false);
6068 	}
6069 
6070 
6071 	/++
6072 		Create a timer with a callback when it triggers.
6073 	+/
6074 	this(int intervalInMilliseconds, void delegate() onPulse, bool repeats = true) @trusted {
6075 		assert(onPulse !is null);
6076 
6077 		initialize();
6078 		setPulseCallback(onPulse);
6079 		changeTime(intervalInMilliseconds, repeats);
6080 	}
6081 
6082 	/++
6083 		Sets a one-of timer that happens some time after the given timestamp, then destroys itself
6084 	+/
6085 	this(SimplifiedUtcTimestamp when, void delegate() onTimeArrived) {
6086 		import core.stdc.time;
6087 		auto ts = when.toUnixTime;
6088 		auto now = time(null);
6089 		if(ts <= now) {
6090 			this(false);
6091 			onTimeArrived();
6092 		} else {
6093 			// FIXME: should use the OS facilities to set the actual time on the real time clock
6094 			auto dis = this;
6095 			this(cast(int)(ts - now) * 1000, () {
6096 				onTimeArrived();
6097 				dis.cancel();
6098 				dis.dispose();
6099 			}, false);
6100 		}
6101 	}
6102 
6103 	version(Windows) {} else version(Arsd_core_kqueue) {} else {
6104 		ICoreEventLoop.UnregisterToken unregisterToken;
6105 	}
6106 
6107 	// just cuz I sometimes call it this.
6108 	alias dispose = destroy;
6109 
6110 	/++
6111 		Stop and destroy the timer object.
6112 
6113 		You should not use it again after destroying it.
6114 	+/
6115 	void destroy() {
6116 		version(Windows) {
6117 			cbh.release();
6118 			staticDestroy(handle);
6119 			handle = null;
6120 		} else version(linux) {
6121 			unregisterToken.unregister();
6122 			staticDestroy(fd);
6123 			fd = -1;
6124 		} else version(Arsd_core_kqueue) {
6125 		} else throw new NotYetImplementedException();
6126 	}
6127 
6128 	~this() {
6129 		version(Windows) {
6130 			if(handle)
6131 				cleanupQueue.queue!staticDestroy(handle);
6132 		} else version(linux) {
6133 			cleanupQueue.queue!unregister(unregisterToken);
6134 			if(fd != -1)
6135 				cleanupQueue.queue!staticDestroy(fd);
6136 		}
6137 	}
6138 
6139 
6140 	private:
6141 
6142 	version(Windows)
6143 	static void staticDestroy(HANDLE handle) {
6144 		if(handle) {
6145 			// KillTimer(null, handle);
6146 			CancelWaitableTimer(cast(void*)handle);
6147 			CloseHandle(handle);
6148 		}
6149 	}
6150 	else version(linux)
6151 	static void staticDestroy(int fd) @system {
6152 		if(fd != -1) {
6153 			import unix = core.sys.posix.unistd;
6154 
6155 			unix.close(fd);
6156 		}
6157 	}
6158 
6159 	version(Windows) {} else
6160 	static void unregister(arsd.core.ICoreEventLoop.UnregisterToken urt) {
6161 		if(urt.impl !is null)
6162 			urt.unregister();
6163 	}
6164 
6165 
6166 	void delegate() onPulse;
6167 	int intervalInMilliseconds;
6168 	bool repeats;
6169 
6170 	int lastEventLoopRoundTriggered;
6171 
6172 	version(linux) {
6173 		static auto makeItimerspec(int intervalInMilliseconds, bool repeats) {
6174 			import core.sys.linux.timerfd;
6175 
6176 			itimerspec value;
6177 			value.it_value.tv_sec = cast(int) (intervalInMilliseconds / 1000);
6178 			value.it_value.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
6179 
6180 			if(repeats) {
6181 				value.it_interval.tv_sec = cast(int) (intervalInMilliseconds / 1000);
6182 				value.it_interval.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
6183 			}
6184 
6185 			return value;
6186 		}
6187 	}
6188 
6189 	void trigger() {
6190 		version(linux) {
6191 			import unix = core.sys.posix.unistd;
6192 			long val;
6193 			unix.read(fd, &val, val.sizeof); // gotta clear the pipe
6194 		} else version(Windows) {
6195 			if(this.lastEventLoopRoundTriggered == eventLoopRound)
6196 				return; // never try to actually run faster than the event loop
6197 			lastEventLoopRoundTriggered = eventLoopRound;
6198 		} else version(Arsd_core_kqueue) {
6199 		} else throw new NotYetImplementedException();
6200 
6201 		if(onPulse)
6202 			onPulse();
6203 	}
6204 
6205 	version(Windows)
6206 		extern(Windows)
6207 		//static void timerCallback(HWND, UINT, UINT_PTR timer, DWORD dwTime) nothrow {
6208 		static void timerCallback(void* timer, DWORD lowTime, DWORD hiTime) nothrow {
6209 			auto cbh = cast(CallbackHelper) timer;
6210 			try
6211 				cbh.call();
6212 			catch(Throwable e) { sdpy_abort(e); assert(0); }
6213 		}
6214 
6215 	version(Windows) {
6216 		HANDLE handle;
6217 		CallbackHelper cbh;
6218 	} else version(linux) {
6219 		int fd = -1;
6220 	} else version(Arsd_core_kqueue) {
6221 		int ident;
6222 		static int identTicker;
6223 		CallbackHelper cbh;
6224 	} else static if(UseCocoa) {
6225 	} else static assert(0, "timer not supported");
6226 }
6227 
6228 version(Windows)
6229 	private void sdpy_abort(Throwable e) nothrow {
6230 		try
6231 			MessageBoxA(null, (e.toString() ~ "\0").ptr, "Exception caught in WndProc", 0);
6232 		catch(Exception e)
6233 			MessageBoxA(null, "Exception.toString threw too!", "Exception caught in WndProc", 0);
6234 		ExitProcess(1);
6235 	}
6236 
6237 
6238 private int eventLoopRound = -1; // so things that assume 0 still work eg lastEventLoopRoundTriggered
6239 
6240 
6241 
6242 /++
6243 	For functions that give you an unknown address, you can use this to hold it.
6244 
6245 	Can get:
6246 		ip4
6247 		ip6
6248 		unix
6249 		abstract_
6250 
6251 		name lookup for connect (stream or dgram)
6252 			request canonical name?
6253 
6254 		interface lookup for bind (stream or dgram)
6255 +/
6256 version(HasSocket) struct SocketAddress {
6257 	import core.sys.posix.netdb;
6258 
6259 	/++
6260 		Provides the set of addresses to listen on all supported protocols on the machine for the given interfaces. `localhost` only listens on the loopback interface, whereas `allInterfaces` will listen on loopback as well as the others on the system (meaning it may be publicly exposed to the internet).
6261 
6262 		If you provide a buffer, I recommend using one of length two, so `SocketAddress[2]`, since this usually provides one address for ipv4 and one for ipv6.
6263 	+/
6264 	static SocketAddress[] localhost(ushort port, return UserProvidedBuffer!SocketAddress buffer = null) {
6265 		buffer.append(ip6("::1", port));
6266 		buffer.append(ip4("127.0.0.1", port));
6267 		return buffer.slice;
6268 	}
6269 
6270 	/// ditto
6271 	static SocketAddress[] allInterfaces(ushort port, return UserProvidedBuffer!SocketAddress buffer = null) {
6272 		char[16] str;
6273 		return allInterfaces(intToString(port, str[]), buffer);
6274 	}
6275 
6276 	/// ditto
6277 	static SocketAddress[] allInterfaces(scope const char[] serviceOrPort, return UserProvidedBuffer!SocketAddress buffer = null) {
6278 		addrinfo hints;
6279 		hints.ai_flags = AI_PASSIVE;
6280 		hints.ai_socktype = SOCK_STREAM; // just to filter it down a little tbh
6281 		return get(null, serviceOrPort, &hints, buffer);
6282 	}
6283 
6284 	/++
6285 		Returns a single address object for the given protocol and parameters.
6286 
6287 		You probably should generally prefer [get], [localhost], or [allInterfaces] to have more flexible code.
6288 	+/
6289 	static SocketAddress ip4(scope const char[] address, ushort port, bool forListening = false) {
6290 		return getSingleAddress(AF_INET, AI_NUMERICHOST | (forListening ? AI_PASSIVE : 0), address, port);
6291 	}
6292 
6293 	/// ditto
6294 	static SocketAddress ip4(ushort port) {
6295 		return ip4(null, port, true);
6296 	}
6297 
6298 	/// ditto
6299 	static SocketAddress ip6(scope const char[] address, ushort port, bool forListening = false) {
6300 		return getSingleAddress(AF_INET6, AI_NUMERICHOST | (forListening ? AI_PASSIVE : 0), address, port);
6301 	}
6302 
6303 	/// ditto
6304 	static SocketAddress ip6(ushort port) {
6305 		return ip6(null, port, true);
6306 	}
6307 
6308 	/// ditto
6309 	static SocketAddress unix(scope const char[] path) {
6310 		// FIXME
6311 		SocketAddress addr;
6312 		return addr;
6313 	}
6314 
6315 	/// ditto
6316 	static SocketAddress abstract_(scope const char[] path) {
6317 		char[190] buffer = void;
6318 		buffer[0] = 0;
6319 		buffer[1 .. path.length] = path[];
6320 		return unix(buffer[0 .. 1 + path.length]);
6321 	}
6322 
6323 	private static SocketAddress getSingleAddress(int family, int flags, scope const char[] address, ushort port) {
6324 		addrinfo hints;
6325 		hints.ai_family = family;
6326 		hints.ai_flags = flags;
6327 
6328 		char[16] portBuffer;
6329 		char[] portString = intToString(port, portBuffer[]);
6330 
6331 		SocketAddress[1] addr;
6332 		auto res = get(address, portString, &hints, provideBuffer(addr[]));
6333 		if(res.length == 0)
6334 			throw ArsdException!"bad address"(address.idup, port);
6335 		return res[0];
6336 	}
6337 
6338 	/++
6339 		Calls `getaddrinfo` and returns the array of results. It will populate the data into the buffer you provide, if you provide one, otherwise it will allocate its own.
6340 	+/
6341 	static SocketAddress[] get(scope const char[] nodeName, scope const char[] serviceOrPort, addrinfo* hints = null, return UserProvidedBuffer!SocketAddress buffer = null, scope bool delegate(scope addrinfo* ai) filter = null) @trusted {
6342 		addrinfo* res;
6343 		CharzBuffer node = nodeName;
6344 		CharzBuffer service = serviceOrPort;
6345 		auto ret = getaddrinfo(nodeName is null ? null : node.ptr, serviceOrPort is null ? null : service.ptr, hints, &res);
6346 		if(ret == 0) {
6347 			auto current = res;
6348 			while(current) {
6349 				if(filter is null || filter(current)) {
6350 					SocketAddress addr;
6351 					addr.addrlen = cast(socklen_t) current.ai_addrlen;
6352 					switch(current.ai_family) {
6353 						case AF_INET:
6354 							addr.in4 = * cast(sockaddr_in*) current.ai_addr;
6355 							break;
6356 						case AF_INET6:
6357 							addr.in6 = * cast(sockaddr_in6*) current.ai_addr;
6358 							break;
6359 						case AF_UNIX:
6360 							addr.unix_address = * cast(sockaddr_un*) current.ai_addr;
6361 							break;
6362 						default:
6363 							// skip
6364 					}
6365 
6366 					if(!buffer.append(addr))
6367 						break;
6368 				}
6369 
6370 				current = current.ai_next;
6371 			}
6372 
6373 			freeaddrinfo(res);
6374 		} else {
6375 			version(Windows) {
6376 				throw new WindowsApiException("getaddrinfo", ret);
6377 			} else {
6378 				const char* error = gai_strerror(ret);
6379 			}
6380 		}
6381 
6382 		return buffer.slice;
6383 	}
6384 
6385 	/++
6386 		Returns a string representation of the address that identifies it in a custom format.
6387 
6388 		$(LIST
6389 			* Unix domain socket addresses are their path prefixed with "unix:", unless they are in the abstract namespace, in which case it is prefixed with "abstract:" and the zero is trimmed out. For example, "unix:/tmp/pipe".
6390 
6391 			* IPv4 addresses are written in dotted decimal followed by a colon and the port number. For example, "127.0.0.1:8080".
6392 
6393 			* IPv6 addresses are written in colon separated hex format, but enclosed in brackets, then followed by the colon and port number. For example, "[::1]:8080".
6394 		)
6395 	+/
6396 	string toString() const @trusted {
6397 		char[200] buffer;
6398 		switch(address.sa_family) {
6399 			case AF_INET:
6400 				auto writable = stringz(inet_ntop(address.sa_family, &in4.sin_addr, buffer.ptr, buffer.length));
6401 				auto it = writable.borrow;
6402 				buffer[it.length] = ':';
6403 				auto numbers = intToString(port, buffer[it.length + 1 .. $]);
6404 				return buffer[0 .. it.length + 1 + numbers.length].idup;
6405 			case AF_INET6:
6406 				buffer[0] = '[';
6407 				auto writable = stringz(inet_ntop(address.sa_family, &in6.sin6_addr, buffer.ptr + 1, buffer.length - 1));
6408 				auto it = writable.borrow;
6409 				buffer[it.length + 1] = ']';
6410 				buffer[it.length + 2] = ':';
6411 				auto numbers = intToString(port, buffer[it.length + 3 .. $]);
6412 				return buffer[0 .. it.length + 3 + numbers.length].idup;
6413 			case AF_UNIX:
6414 				// FIXME: it might be abstract in which case stringz is wrong!!!!!
6415 				auto writable = stringz(cast(char*) unix_address.sun_path.ptr).borrow;
6416 				if(writable.length == 0)
6417 					return "unix:";
6418 				string prefix = writable[0] == 0 ? "abstract:" : "unix:";
6419 				buffer[0 .. prefix.length] = prefix[];
6420 				buffer[prefix.length .. prefix.length + writable.length] = writable[writable[0] == 0 ? 1 : 0 .. $];
6421 				return buffer.idup;
6422 			case AF_UNSPEC:
6423 				return "<unspecified address>";
6424 			default:
6425 				return "<unsupported address>"; // FIXME
6426 		}
6427 	}
6428 
6429 	ushort port() const @trusted {
6430 		switch(address.sa_family) {
6431 			case AF_INET:
6432 				return ntohs(in4.sin_port);
6433 			case AF_INET6:
6434 				return ntohs(in6.sin6_port);
6435 			default:
6436 				return 0;
6437 		}
6438 	}
6439 
6440 	/+
6441 	@safe unittest {
6442 		SocketAddress[4] buffer;
6443 		foreach(addr; SocketAddress.get("arsdnet.net", "http", null, provideBuffer(buffer[])))
6444 			writeln(addr.toString());
6445 	}
6446 	+/
6447 
6448 	/+
6449 	unittest {
6450 		// writeln(SocketAddress.ip4(null, 4444, true));
6451 		// writeln(SocketAddress.ip4("400.3.2.1", 4444));
6452 		// writeln(SocketAddress.ip4("bar", 4444));
6453 		foreach(addr; localhost(4444))
6454 			writeln(addr.toString());
6455 	}
6456 	+/
6457 
6458 	socklen_t addrlen = typeof(this).sizeof - socklen_t.sizeof; // the size of the union below
6459 
6460 	union {
6461 		sockaddr address;
6462 
6463 		sockaddr_storage storage;
6464 
6465 		sockaddr_in in4;
6466 		sockaddr_in6 in6;
6467 
6468 		sockaddr_un unix_address;
6469 	}
6470 
6471 	/+
6472 	this(string node, string serviceOrPort, int family = 0) {
6473 		// need to populate the approrpiate address and the length and make sure you set sa_family
6474 	}
6475 	+/
6476 
6477 	int domain() {
6478 		return address.sa_family;
6479 	}
6480 	sockaddr* rawAddr() return {
6481 		return &address;
6482 	}
6483 	socklen_t rawAddrLength() {
6484 		return addrlen;
6485 	}
6486 
6487 	// FIXME it is AF_BLUETOOTH
6488 	// see: https://people.csail.mit.edu/albert/bluez-intro/x79.html
6489 	// see: https://learn.microsoft.com/en-us/windows/win32/Bluetooth/bluetooth-programming-with-windows-sockets
6490 }
6491 
6492 private version(Windows) {
6493 	struct sockaddr_un {
6494 		ushort sun_family;
6495 		char[108] sun_path;
6496 	}
6497 }
6498 
6499 version(HasFile) class AsyncSocket : AsyncFile {
6500 	// otherwise: accept, bind, connect, shutdown, close.
6501 
6502 	static auto lastError() {
6503 		version(Windows)
6504 			return WSAGetLastError();
6505 		else
6506 			return errno;
6507 	}
6508 
6509 	static bool wouldHaveBlocked() {
6510 		auto error = lastError;
6511 		version(Windows) {
6512 			return error == WSAEWOULDBLOCK || error == WSAETIMEDOUT;
6513 		} else {
6514 			return error == EAGAIN || error == EWOULDBLOCK;
6515 		}
6516 	}
6517 
6518 	version(Windows)
6519 		enum INVALID = INVALID_SOCKET;
6520 	else
6521 		enum INVALID = -1;
6522 
6523 	// type is mostly SOCK_STREAM or SOCK_DGRAM
6524 	/++
6525 		Creates a socket compatible with the given address. It does not actually connect or bind, nor store the address. You will want to pass it again to those functions:
6526 
6527 		---
6528 		auto socket = new Socket(address, Socket.Type.Stream);
6529 		socket.connect(address).waitForCompletion();
6530 		---
6531 	+/
6532 	this(SocketAddress address, int type, int protocol = 0) {
6533 		// need to look up these values for linux
6534 		// type |= SOCK_NONBLOCK | SOCK_CLOEXEC;
6535 
6536 		handle_ = socket(address.domain(), type, protocol);
6537 		if(handle == INVALID)
6538 			throw new SystemApiException("socket", lastError());
6539 
6540 		super(cast(NativeFileHandle) handle); // I think that cast is ok on Windows... i think
6541 
6542 		version(Posix) {
6543 			makeNonBlocking(handle);
6544 			setCloExec(handle);
6545 		}
6546 
6547 		if(address.domain == AF_INET6) {
6548 			int opt = 1;
6549 			setsockopt(handle, IPPROTO_IPV6 /*SOL_IPV6*/, IPV6_V6ONLY, &opt, opt.sizeof);
6550 		}
6551 
6552 		// FIXME: chekc for broadcast
6553 
6554 		// FIXME: REUSEADDR ?
6555 
6556 		// FIXME: also set NO_DELAY prolly
6557 		// int opt = 1;
6558 		// setsockopt(handle, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof);
6559 	}
6560 
6561 	/++
6562 		Enabling NODELAY can give latency improvements if you are managing buffers on your end
6563 	+/
6564 	void setNoDelay(bool enabled) {
6565 
6566 	}
6567 
6568 	/++
6569 
6570 		`allowQuickRestart` will set the SO_REUSEADDR on unix and SO_DONTLINGER on Windows,
6571 		allowing the application to be quickly restarted despite there still potentially being
6572 		pending data in the tcp stack.
6573 
6574 		See https://stackoverflow.com/questions/3229860/what-is-the-meaning-of-so-reuseaddr-setsockopt-option-linux for more information.
6575 
6576 		If you already set your appropriate socket options or value correctness and reliability of the network stream over restart speed, leave this at the default `false`.
6577 	+/
6578 	void bind(SocketAddress address, bool allowQuickRestart = false) {
6579 		if(allowQuickRestart) {
6580 			// FIXME
6581 		}
6582 
6583 		auto ret = .bind(handle, address.rawAddr, address.rawAddrLength);
6584 		if(ret == -1)
6585 			throw new SystemApiException("bind", lastError);
6586 	}
6587 
6588 	/++
6589 		You must call [bind] before this.
6590 
6591 		The backlog should be set to a value where your application can reliably catch up on the backlog in a reasonable amount of time under average load. It is meant to smooth over short duration bursts and making it too big will leave clients hanging - which might cause them to try to reconnect, thinking things got lost in transit, adding to your impossible backlog.
6592 
6593 		I personally tend to set this to be two per worker thread unless I have actual real world measurements saying to do something else. It is a bit arbitrary and not based on legitimate reasoning, it just seems to work for me (perhaps just because it has never really been put to the test).
6594 	+/
6595 	void listen(int backlog) {
6596 		auto ret = .listen(handle, backlog);
6597 		if(ret == -1)
6598 			throw new SystemApiException("listen", lastError);
6599 	}
6600 
6601 	/++
6602 	+/
6603 	void shutdown(int how) {
6604 		auto ret = .shutdown(handle, how);
6605 		if(ret == -1)
6606 			throw new SystemApiException("shutdown", lastError);
6607 	}
6608 
6609 	/++
6610 	+/
6611 	override void close() {
6612 		version(Windows)
6613 			closesocket(handle);
6614 		else
6615 			.close(handle);
6616 		handle_ = -1;
6617 	}
6618 
6619 	/++
6620 		You can also construct your own request externally to control the memory more.
6621 	+/
6622 	AsyncConnectRequest connect(SocketAddress address, ubyte[] bufferToSend = null) {
6623 		return new AsyncConnectRequest(this, address, bufferToSend);
6624 	}
6625 
6626 	/++
6627 		You can also construct your own request externally to control the memory more.
6628 	+/
6629 	AsyncAcceptRequest accept() {
6630 		return new AsyncAcceptRequest(this);
6631 	}
6632 
6633 	// note that send is just sendto w/ a null address
6634 	// and receive is just receivefrom w/ a null address
6635 	/++
6636 		You can also construct your own request externally to control the memory more.
6637 	+/
6638 	AsyncSendRequest send(const(ubyte)[] buffer, int flags = 0) {
6639 		return new AsyncSendRequest(this, buffer, null, flags);
6640 	}
6641 
6642 	/++
6643 		You can also construct your own request externally to control the memory more.
6644 	+/
6645 	AsyncReceiveRequest receive(ubyte[] buffer, int flags = 0) {
6646 		return new AsyncReceiveRequest(this, buffer, null, flags);
6647 	}
6648 
6649 	/++
6650 		You can also construct your own request externally to control the memory more.
6651 	+/
6652 	AsyncSendRequest sendTo(const(ubyte)[] buffer, SocketAddress* address, int flags = 0) {
6653 		return new AsyncSendRequest(this, buffer, address, flags);
6654 	}
6655 	/++
6656 		You can also construct your own request externally to control the memory more.
6657 	+/
6658 	AsyncReceiveRequest receiveFrom(ubyte[] buffer, SocketAddress* address, int flags = 0) {
6659 		return new AsyncReceiveRequest(this, buffer, address, flags);
6660 	}
6661 
6662 	/++
6663 	+/
6664 	SocketAddress localAddress() {
6665 		SocketAddress addr;
6666 		getsockname(handle, &addr.address, &addr.addrlen);
6667 		return addr;
6668 	}
6669 	/++
6670 	+/
6671 	SocketAddress peerAddress() {
6672 		SocketAddress addr;
6673 		getpeername(handle, &addr.address, &addr.addrlen);
6674 		return addr;
6675 	}
6676 
6677 	// for unix sockets on unix only: send/receive fd, get peer creds
6678 
6679 	/++
6680 
6681 	+/
6682 	final NativeSocketHandle handle() {
6683 		return handle_;
6684 	}
6685 
6686 	private NativeSocketHandle handle_;
6687 }
6688 
6689 /++
6690 	Initiates a connection request and optionally sends initial data as soon as possible.
6691 
6692 	Calls `ConnectEx` on Windows and emulates it on other systems.
6693 
6694 	The entire buffer is sent before the operation is considered complete.
6695 
6696 	NOT IMPLEMENTED / NOT STABLE
6697 +/
6698 version(HasSocket) class AsyncConnectRequest : AsyncOperationRequest {
6699 	// FIXME: i should take a list of addresses and take the first one that succeeds, so a getaddrinfo can be sent straight in.
6700 	this(AsyncSocket socket, SocketAddress address, ubyte[] dataToWrite) {
6701 
6702 	}
6703 
6704 	override void start() {}
6705 	override void cancel() {}
6706 	override bool isComplete() { return true; }
6707 	override AsyncConnectResponse waitForCompletion() { assert(0); }
6708 }
6709 /++
6710 +/
6711 version(HasSocket) class AsyncConnectResponse : AsyncOperationResponse {
6712 	const SystemErrorCode errorCode;
6713 
6714 	this(SystemErrorCode errorCode) {
6715 		this.errorCode = errorCode;
6716 	}
6717 
6718 	override bool wasSuccessful() {
6719 		return errorCode.wasSuccessful;
6720 	}
6721 
6722 }
6723 
6724 // FIXME: TransmitFile/sendfile support
6725 
6726 /++
6727 	Calls `AcceptEx` on Windows and emulates it on other systems.
6728 
6729 	NOT IMPLEMENTED / NOT STABLE
6730 +/
6731 version(HasSocket) class AsyncAcceptRequest : AsyncOperationRequest {
6732 	AsyncSocket socket;
6733 
6734 	override void start() {}
6735 	override void cancel() {}
6736 	override bool isComplete() { return true; }
6737 	override AsyncConnectResponse waitForCompletion() { assert(0); }
6738 
6739 
6740 	struct LowLevelOperation {
6741 		AsyncSocket file;
6742 		ubyte[] buffer;
6743 		SocketAddress* address;
6744 
6745 		this(typeof(this.tupleof) args) {
6746 			this.tupleof = args;
6747 		}
6748 
6749 		version(Windows) {
6750 			auto opCall(OVERLAPPED* overlapped, LPOVERLAPPED_COMPLETION_ROUTINE ocr) {
6751 				WSABUF buf;
6752 				buf.len = cast(int) buffer.length;
6753 				buf.buf = cast(typeof(buf.buf)) buffer.ptr;
6754 
6755 				uint flags;
6756 
6757 				if(address is null)
6758 					return WSARecv(file.handle, &buf, 1, null, &flags, overlapped, ocr);
6759 				else {
6760 					return WSARecvFrom(file.handle, &buf, 1, null, &flags, &(address.address), &(address.addrlen), overlapped, ocr);
6761 				}
6762 			}
6763 		} else {
6764 			auto opCall() {
6765 				int flags;
6766 				if(address is null)
6767 					return core.sys.posix.sys.socket.recv(file.handle, buffer.ptr, buffer.length, flags);
6768 				else
6769 					return core.sys.posix.sys.socket.recvfrom(file.handle, buffer.ptr, buffer.length, flags, &(address.address), &(address.addrlen));
6770 			}
6771 		}
6772 
6773 		string errorString() {
6774 			return "Receive";
6775 		}
6776 	}
6777 	mixin OverlappedIoRequest!(AsyncAcceptResponse, LowLevelOperation);
6778 
6779 	this(AsyncSocket socket, ubyte[] buffer = null, SocketAddress* address = null) {
6780 		llo = LowLevelOperation(socket, buffer, address);
6781 		this.response = typeof(this.response).defaultConstructed;
6782 	}
6783 
6784 	// can also look up the local address
6785 }
6786 /++
6787 +/
6788 version(HasSocket) class AsyncAcceptResponse : AsyncOperationResponse {
6789 	AsyncSocket newSocket;
6790 	const SystemErrorCode errorCode;
6791 
6792 	this(SystemErrorCode errorCode, ubyte[] buffer) {
6793 		this.errorCode = errorCode;
6794 	}
6795 
6796 	this(AsyncSocket newSocket, SystemErrorCode errorCode) {
6797 		this.newSocket = newSocket;
6798 		this.errorCode = errorCode;
6799 	}
6800 
6801 	override bool wasSuccessful() {
6802 		return errorCode.wasSuccessful;
6803 	}
6804 }
6805 
6806 /++
6807 +/
6808 version(HasSocket) class AsyncReceiveRequest : AsyncOperationRequest {
6809 	struct LowLevelOperation {
6810 		AsyncSocket file;
6811 		ubyte[] buffer;
6812 		int flags;
6813 		SocketAddress* address;
6814 
6815 		this(typeof(this.tupleof) args) {
6816 			this.tupleof = args;
6817 		}
6818 
6819 		version(Windows) {
6820 			auto opCall(OVERLAPPED* overlapped, LPOVERLAPPED_COMPLETION_ROUTINE ocr) {
6821 				WSABUF buf;
6822 				buf.len = cast(int) buffer.length;
6823 				buf.buf = cast(typeof(buf.buf)) buffer.ptr;
6824 
6825 				uint flags = this.flags;
6826 
6827 				if(address is null)
6828 					return WSARecv(file.handle, &buf, 1, null, &flags, overlapped, ocr);
6829 				else {
6830 					return WSARecvFrom(file.handle, &buf, 1, null, &flags, &(address.address), &(address.addrlen), overlapped, ocr);
6831 				}
6832 			}
6833 		} else {
6834 			auto opCall() {
6835 				if(address is null)
6836 					return core.sys.posix.sys.socket.recv(file.handle, buffer.ptr, buffer.length, flags);
6837 				else
6838 					return core.sys.posix.sys.socket.recvfrom(file.handle, buffer.ptr, buffer.length, flags, &(address.address), &(address.addrlen));
6839 			}
6840 		}
6841 
6842 		string errorString() {
6843 			return "Receive";
6844 		}
6845 	}
6846 	mixin OverlappedIoRequest!(AsyncReceiveResponse, LowLevelOperation);
6847 
6848 	this(AsyncSocket socket, ubyte[] buffer, SocketAddress* address, int flags) {
6849 		llo = LowLevelOperation(socket, buffer, flags, address);
6850 		this.response = typeof(this.response).defaultConstructed;
6851 	}
6852 
6853 }
6854 /++
6855 +/
6856 version(HasSocket) class AsyncReceiveResponse : AsyncOperationResponse {
6857 	const ubyte[] bufferWritten;
6858 	const SystemErrorCode errorCode;
6859 
6860 	this(SystemErrorCode errorCode, const(ubyte)[] bufferWritten) {
6861 		this.errorCode = errorCode;
6862 		this.bufferWritten = bufferWritten;
6863 	}
6864 
6865 	override bool wasSuccessful() {
6866 		return errorCode.wasSuccessful;
6867 	}
6868 }
6869 
6870 /++
6871 +/
6872 version(HasSocket) class AsyncSendRequest : AsyncOperationRequest {
6873 	struct LowLevelOperation {
6874 		AsyncSocket file;
6875 		const(ubyte)[] buffer;
6876 		int flags;
6877 		SocketAddress* address;
6878 
6879 		this(typeof(this.tupleof) args) {
6880 			this.tupleof = args;
6881 		}
6882 
6883 		version(Windows) {
6884 			auto opCall(OVERLAPPED* overlapped, LPOVERLAPPED_COMPLETION_ROUTINE ocr) {
6885 				WSABUF buf;
6886 				buf.len = cast(int) buffer.length;
6887 				buf.buf = cast(typeof(buf.buf)) buffer.ptr;
6888 
6889 				if(address is null)
6890 					return WSASend(file.handle, &buf, 1, null, flags, overlapped, ocr);
6891 				else {
6892 					return WSASendTo(file.handle, &buf, 1, null, flags, address.rawAddr, address.rawAddrLength, overlapped, ocr);
6893 				}
6894 			}
6895 		} else {
6896 			auto opCall() {
6897 				if(address is null)
6898 					return core.sys.posix.sys.socket.send(file.handle, buffer.ptr, buffer.length, flags);
6899 				else
6900 					return core.sys.posix.sys.socket.sendto(file.handle, buffer.ptr, buffer.length, flags, address.rawAddr, address.rawAddrLength);
6901 			}
6902 		}
6903 
6904 		string errorString() {
6905 			return "Send";
6906 		}
6907 	}
6908 	mixin OverlappedIoRequest!(AsyncSendResponse, LowLevelOperation);
6909 
6910 	this(AsyncSocket socket, const(ubyte)[] buffer, SocketAddress* address, int flags) {
6911 		llo = LowLevelOperation(socket, buffer, flags, address);
6912 		this.response = typeof(this.response).defaultConstructed;
6913 	}
6914 }
6915 
6916 /++
6917 +/
6918 version(HasSocket) class AsyncSendResponse : AsyncOperationResponse {
6919 	const ubyte[] bufferWritten;
6920 	const SystemErrorCode errorCode;
6921 
6922 	this(SystemErrorCode errorCode, const(ubyte)[] bufferWritten) {
6923 		this.errorCode = errorCode;
6924 		this.bufferWritten = bufferWritten;
6925 	}
6926 
6927 	override bool wasSuccessful() {
6928 		return errorCode.wasSuccessful;
6929 	}
6930 
6931 }
6932 
6933 /++
6934 	A set of sockets bound and ready to accept connections on worker threads.
6935 
6936 	Depending on the specified address, it can be tcp, tcpv6, unix domain, or all of the above.
6937 
6938 	NOT IMPLEMENTED / NOT STABLE
6939 +/
6940 version(HasSocket) class StreamServer {
6941 	AsyncSocket[] sockets;
6942 
6943 	this(SocketAddress[] listenTo, int backlog = 8) {
6944 		foreach(listen; listenTo) {
6945 			auto socket = new AsyncSocket(listen, SOCK_STREAM);
6946 
6947 			// FIXME: allInterfaces for ipv6 also covers ipv4 so the bind can fail...
6948 			// so we have to permit it to fail w/ address in use if we know we already
6949 			// are listening to ipv6
6950 
6951 			// or there is a setsockopt ipv6 only thing i could set.
6952 
6953 			socket.bind(listen);
6954 			socket.listen(backlog);
6955 			sockets ~= socket;
6956 
6957 			// writeln(socket.localAddress.port);
6958 		}
6959 
6960 		// i have to start accepting on each thread for each socket...
6961 	}
6962 	// when a new connection arrives, it calls your callback
6963 	// can be on a specific thread or on any thread
6964 
6965 
6966 	void start() {
6967 		foreach(socket; sockets) {
6968 			auto request = socket.accept();
6969 			request.start();
6970 		}
6971 	}
6972 }
6973 
6974 /+
6975 unittest {
6976 	auto ss = new StreamServer(SocketAddress.localhost(0));
6977 }
6978 +/
6979 
6980 /++
6981 	A socket bound and ready to use receiveFrom
6982 
6983 	Depending on the address, it can be udp or unix domain.
6984 
6985 	NOT IMPLEMENTED / NOT STABLE
6986 +/
6987 version(HasSocket) class DatagramListener {
6988 	// whenever a udp message arrives, it calls your callback
6989 	// can be on a specific thread or on any thread
6990 
6991 	// UDP is realistically just an async read on the bound socket
6992 	// just it can get the "from" data out and might need the "more in packet" flag
6993 }
6994 
6995 /++
6996 	Just in case I decide to change the implementation some day.
6997 +/
6998 version(HasFile) alias AsyncAnonymousPipe = AsyncFile;
6999 
7000 
7001 // AsyncAnonymousPipe connectNamedPipe(AsyncAnonymousPipe preallocated, string name)
7002 
7003 // unix fifos are considered just non-seekable files and have no special support in the lib; open them as a regular file w/ the async flag.
7004 
7005 // DIRECTORY LISTINGS
7006 	// not async, so if you want that, do it in a helper thread
7007 	// just a convenient function to have (tho phobos has a decent one too, importing it expensive af)
7008 
7009 /++
7010 	Note that the order of items called for your delegate is undefined; if you want it sorted, you'll have to collect and sort yourself. But it *might* be sorted by the OS (on Windows, it almost always is), so consider that when choosing a sorting algorithm.
7011 
7012 	History:
7013 		previously in minigui as a private function. Moved to arsd.core on April 3, 2023
7014 +/
7015 version(HasFile) GetFilesResult getFiles(string directory, scope void delegate(string name, bool isDirectory) dg) {
7016 	// FIXME: my buffers here aren't great lol
7017 
7018 	SavedArgument[1] argsForException() {
7019 		return [
7020 			SavedArgument("directory", LimitedVariant(directory)),
7021 		];
7022 	}
7023 
7024 	version(Windows) {
7025 		WIN32_FIND_DATA data;
7026 		// FIXME: if directory ends with / or \\ ?
7027 		WCharzBuffer search = WCharzBuffer(directory ~ "/*");
7028 		auto handle = FindFirstFileW(search.ptr, &data);
7029 		scope(exit) if(handle !is INVALID_HANDLE_VALUE) FindClose(handle);
7030 		if(handle is INVALID_HANDLE_VALUE) {
7031 			if(GetLastError() == ERROR_FILE_NOT_FOUND)
7032 				return GetFilesResult.fileNotFound;
7033 			throw new WindowsApiException("FindFirstFileW", GetLastError(), argsForException()[]);
7034 		}
7035 
7036 		try_more:
7037 
7038 		string name = makeUtf8StringFromWindowsString(data.cFileName[0 .. findIndexOfZero(data.cFileName[])]);
7039 
7040 		/+
7041   FILETIME ftLastWriteTime;
7042   DWORD    nFileSizeHigh;
7043   DWORD    nFileSizeLow;
7044 
7045   but these not available on linux w/o statting each file!
7046 		+/
7047 
7048 		dg(name, (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? true : false);
7049 
7050 		auto ret = FindNextFileW(handle, &data);
7051 		if(ret == 0) {
7052 			if(GetLastError() == ERROR_NO_MORE_FILES)
7053 				return GetFilesResult.success;
7054 			throw new WindowsApiException("FindNextFileW", GetLastError(), argsForException()[]);
7055 		}
7056 
7057 		goto try_more;
7058 
7059 	} else version(Posix) {
7060 		import core.sys.posix.dirent;
7061 		import core.stdc.errno;
7062 		auto dir = opendir((directory ~ "\0").ptr);
7063 		scope(exit)
7064 			if(dir) closedir(dir);
7065 		if(dir is null)
7066 			throw new ErrnoApiException("opendir", errno, argsForException());
7067 
7068 		auto dirent = readdir(dir);
7069 		if(dirent is null)
7070 			return GetFilesResult.fileNotFound;
7071 
7072 		try_more:
7073 
7074 		string name = dirent.d_name[0 .. findIndexOfZero(dirent.d_name[])].idup;
7075 
7076 		dg(name, dirent.d_type == DT_DIR);
7077 
7078 		dirent = readdir(dir);
7079 		if(dirent is null)
7080 			return GetFilesResult.success;
7081 
7082 		goto try_more;
7083 	} else static assert(0);
7084 }
7085 
7086 /// ditto
7087 enum GetFilesResult {
7088 	success,
7089 	fileNotFound
7090 }
7091 
7092 /++
7093 	This is currently a simplified glob where only the * wildcard in the first or last position gets special treatment or a single * in the middle.
7094 
7095 	More things may be added later to be more like what Phobos supports.
7096 +/
7097 bool matchesFilePattern(scope const(char)[] name, scope const(char)[] pattern, char star = '*') {
7098 	if(pattern.length == 0)
7099 		return false;
7100 	if(pattern.length == 1 && pattern[0] == star)
7101 		return true;
7102 	if(pattern.length > 2 && pattern[0] == star && pattern[$-1] == star) {
7103 		// if the rest of pattern appears in name, it is good
7104 		return name.indexOf(pattern[1 .. $-1]) != -1;
7105 	} else if(pattern[0] == star) {
7106 		// if the rest of pattern is at end of name, it is good
7107 		return name.endsWith(pattern[1 .. $]);
7108 	} else if(pattern[$-1] == star) {
7109 		// if the rest of pattern is at start of name, it is good
7110 		return name.startsWith(pattern[0 .. $-1]);
7111 	} else if(pattern.length >= 3) {
7112 		char[1] starString = star;
7113 		auto idx = pattern.indexOf(starString[]);
7114 		if(idx != -1) {
7115 			auto lhs = pattern[0 .. idx];
7116 			auto rhs = pattern[idx + 1 .. $];
7117 			if(name.length >= lhs.length + rhs.length) {
7118 				return name.startsWith(lhs) && name.endsWith(rhs);
7119 			} else {
7120 				return false;
7121 			}
7122 		}
7123 	}
7124 
7125 	return name == pattern;
7126 }
7127 
7128 unittest {
7129 	assert("test.html".matchesFilePattern("*"));
7130 	assert("test.html".matchesFilePattern("*.html"));
7131 	assert("test.html".matchesFilePattern("*.*"));
7132 	assert("test.html".matchesFilePattern("test.*"));
7133 	assert(!"test.html".matchesFilePattern("pest.*"));
7134 	assert(!"test.html".matchesFilePattern("*.dhtml"));
7135 
7136 	assert("test.html".matchesFilePattern("t*.html"));
7137 	assert(!"test.html".matchesFilePattern("e*.html"));
7138 }
7139 
7140 package(arsd) int indexOf(scope const(char)[] haystack, scope const(char)[] needle) {
7141 	if(haystack.length < needle.length)
7142 		return -1;
7143 	if(haystack == needle)
7144 		return 0;
7145 	foreach(i; 0 .. haystack.length - needle.length + 1)
7146 		if(haystack[i .. i + needle.length] == needle)
7147 			return cast(int) i;
7148 	return -1;
7149 }
7150 
7151 package(arsd) int indexOf(scope const(ubyte)[] haystack, scope const(char)[] needle) {
7152 	return indexOf(cast(const(char)[]) haystack, needle);
7153 }
7154 
7155 unittest {
7156 	assert("foo".indexOf("f") == 0);
7157 	assert("foo".indexOf("o") == 1);
7158 	assert("foo".indexOf("foo") == 0);
7159 	assert("foo".indexOf("oo") == 1);
7160 	assert("foo".indexOf("fo") == 0);
7161 	assert("foo".indexOf("boo") == -1);
7162 	assert("foo".indexOf("food") == -1);
7163 }
7164 
7165 package(arsd) bool endsWith(scope const(char)[] haystack, scope const(char)[] needle) {
7166 	if(needle.length > haystack.length)
7167 		return false;
7168 	return haystack[$ - needle.length .. $] == needle;
7169 }
7170 
7171 unittest {
7172 	assert("foo".endsWith("o"));
7173 	assert("foo".endsWith("oo"));
7174 	assert("foo".endsWith("foo"));
7175 	assert(!"foo".endsWith("food"));
7176 	assert(!"foo".endsWith("d"));
7177 }
7178 
7179 package(arsd) bool startsWith(scope const(char)[] haystack, scope const(char)[] needle) {
7180 	if(needle.length > haystack.length)
7181 		return false;
7182 	return haystack[0 .. needle.length] == needle;
7183 }
7184 
7185 unittest {
7186 	assert("foo".startsWith("f"));
7187 	assert("foo".startsWith("fo"));
7188 	assert("foo".startsWith("foo"));
7189 	assert(!"foo".startsWith("food"));
7190 	assert(!"foo".startsWith("d"));
7191 }
7192 
7193 
7194 // FILE/DIR WATCHES
7195 	// linux does it by name, windows and bsd do it by handle/descriptor
7196 	// dispatches change event to either your thread or maybe the any task` queue.
7197 
7198 /++
7199 	PARTIALLY IMPLEMENTED / NOT STABLE
7200 
7201 +/
7202 class DirectoryWatcher {
7203 	private {
7204 		version(Arsd_core_windows) {
7205 			OVERLAPPED overlapped;
7206 			HANDLE hDirectory;
7207 			ubyte[] buffer;
7208 
7209 			extern(Windows)
7210 			static void overlappedCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransferred, LPOVERLAPPED lpOverlapped) @system {
7211 				typeof(this) rr = cast(typeof(this)) (cast(void*) lpOverlapped - typeof(this).overlapped.offsetof);
7212 
7213 				// dwErrorCode
7214 				auto response = rr.buffer[0 .. dwNumberOfBytesTransferred];
7215 
7216 				while(response.length) {
7217 					auto fni = cast(FILE_NOTIFY_INFORMATION*) response.ptr;
7218 					auto filename = fni.FileName[0 .. fni.FileNameLength];
7219 
7220 					if(fni.NextEntryOffset)
7221 						response = response[fni.NextEntryOffset .. $];
7222 					else
7223 						response = response[$..$];
7224 
7225 					// FIXME: I think I need to pin every overlapped op while it is pending
7226 					// and unpin it when it is returned. GC.addRoot... but i don't wanna do that
7227 					// every op so i guess i should do a refcount scheme similar to the other callback helper.
7228 
7229 					rr.changeHandler(
7230 						FilePath(makeUtf8StringFromWindowsString(filename)), // FIXME: this is a relative path
7231 						ChangeOperation.unknown // FIXME this is fni.Action
7232 					);
7233 				}
7234 
7235 				rr.requestRead();
7236 			}
7237 
7238 			void requestRead() {
7239 				DWORD ignored;
7240 				if(!ReadDirectoryChangesW(
7241 					hDirectory,
7242 					buffer.ptr,
7243 					cast(int) buffer.length,
7244 					recursive,
7245 					FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_FILE_NAME,
7246 					&ignored,
7247 					&overlapped,
7248 					&overlappedCompletionRoutine
7249 				)) {
7250 					auto error = GetLastError();
7251 					/+
7252 					if(error == ERROR_IO_PENDING) {
7253 						// not expected here, the docs say it returns true when queued
7254 					}
7255 					+/
7256 
7257 					throw new SystemApiException("ReadDirectoryChangesW", error);
7258 				}
7259 			}
7260 		} else version(Arsd_core_epoll) {
7261 			static int inotifyfd = -1; // this is TLS since it is associated with the thread's event loop
7262 			static ICoreEventLoop.UnregisterToken inotifyToken;
7263 			static CallbackHelper inotifycb;
7264 			static DirectoryWatcher[int] watchMappings;
7265 
7266 			static ~this() {
7267 				if(inotifyfd != -1) {
7268 					close(inotifyfd);
7269 					inotifyfd = -1;
7270 				}
7271 			}
7272 
7273 			import core.sys.linux.sys.inotify;
7274 
7275 			int watchId = -1;
7276 
7277 			static void inotifyReady() {
7278 				// read from it
7279 				ubyte[256 /* NAME_MAX + 1 */ + inotify_event.sizeof] sbuffer;
7280 
7281 				auto ret = read(inotifyfd, sbuffer.ptr, sbuffer.length);
7282 				if(ret == -1) {
7283 					auto errno = errno;
7284 					if(errno == EAGAIN || errno == EWOULDBLOCK)
7285 						return;
7286 					throw new SystemApiException("read inotify", errno);
7287 				} else if(ret == 0) {
7288 					assert(0, "I don't think this is ever supposed to happen");
7289 				}
7290 
7291 				auto buffer = sbuffer[0 .. ret];
7292 
7293 				while(buffer.length > 0) {
7294 					inotify_event* event = cast(inotify_event*) buffer.ptr;
7295 					buffer = buffer[inotify_event.sizeof .. $];
7296 					char[] filename = cast(char[]) buffer[0 .. event.len];
7297 					buffer = buffer[event.len .. $];
7298 
7299 					// note that filename is padded with zeroes, so it is actually a stringz
7300 
7301 					if(auto obj = event.wd in watchMappings) {
7302 						(*obj).changeHandler(
7303 							FilePath(stringz(filename.ptr).borrow.idup), // FIXME: this is a relative path
7304 							ChangeOperation.unknown // FIXME
7305 						);
7306 					} else {
7307 						// it has probably already been removed
7308 					}
7309 				}
7310 			}
7311 		} else version(Arsd_core_kqueue) {
7312 			int fd;
7313 			CallbackHelper cb;
7314 		}
7315 
7316 		FilePath path;
7317 		string globPattern;
7318 		bool recursive;
7319 		void delegate(FilePath filename, ChangeOperation op) changeHandler;
7320 	}
7321 
7322 	enum ChangeOperation {
7323 		unknown,
7324 		deleted, // NOTE_DELETE, IN_DELETE, FILE_NOTIFY_CHANGE_FILE_NAME
7325 		written, // NOTE_WRITE / NOTE_EXTEND / NOTE_TRUNCATE, IN_MODIFY, FILE_NOTIFY_CHANGE_LAST_WRITE / FILE_NOTIFY_CHANGE_SIZE
7326 		renamed, // NOTE_RENAME, the moved from/to in linux, FILE_NOTIFY_CHANGE_FILE_NAME
7327 		metadataChanged // NOTE_ATTRIB, IN_ATTRIB, FILE_NOTIFY_CHANGE_ATTRIBUTES
7328 
7329 		// there is a NOTE_OPEN on freebsd 13, and the access change on Windows. and an open thing on linux. so maybe i can do note open/note_read too.
7330 	}
7331 
7332 	/+
7333 		Windows and Linux work best when you watch directories. The operating system tells you the name of files as they change.
7334 
7335 		BSD doesn't support this. You can only get names and reports when a file is modified by watching specific files. AS such, when you watch a directory on those systems, your delegate will be called with a null path. Cross-platform applications should check for this and not assume the name is always usable.
7336 
7337 		inotify is kinda clearly the best of the bunch, with Windows in second place, and kqueue dead last.
7338 
7339 
7340 		If path to watch is a directory, it signals when a file inside the directory (only one layer deep) is created or modified. This is the most efficient on Windows and Linux.
7341 
7342 		If a path is a file, it only signals when that specific file is written. This is most efficient on BSD.
7343 
7344 
7345 		The delegate is called when something happens. Note that the path modified may not be accurate on all systems when you are watching a directory.
7346 	+/
7347 
7348 	/++
7349 		Watches a directory and its contents. If the `globPattern` is `null`, it will not attempt to add child items but also will not filter it, meaning you will be left with platform-specific behavior.
7350 
7351 		On Windows, the globPattern is just used to filter events.
7352 
7353 		On Linux, the `recursive` flag, if set, will cause it to add additional OS-level watches for each subdirectory.
7354 
7355 		On BSD, anything other than a null pattern will cause a directory scan to add files to the watch list.
7356 
7357 		For best results, use the most limited thing you need, as watches can get quite involved on the bsd systems.
7358 
7359 		Newly added files and subdirectories may not be automatically added in all cases, meaning if it is added and then subsequently modified, you might miss a notification.
7360 
7361 		If the event queue is too busy, the OS may skip a notification.
7362 
7363 		You should always offer some way for the user to force a refresh and not rely on notifications being present; they are a convenience when they work, not an always reliable method.
7364 	+/
7365 	this(FilePath directoryToWatch, string globPattern, bool recursive, void delegate(FilePath pathModified, ChangeOperation op) dg) {
7366 		this.path = directoryToWatch;
7367 		this.globPattern = globPattern;
7368 		this.recursive = recursive;
7369 		this.changeHandler = dg;
7370 
7371 		version(Arsd_core_windows) {
7372 			WCharzBuffer wname = directoryToWatch.path;
7373 			buffer = new ubyte[](1024);
7374 			hDirectory = CreateFileW(
7375 				wname.ptr,
7376 				GENERIC_READ,
7377 				FILE_SHARE_READ,
7378 				null,
7379 				OPEN_EXISTING,
7380 				FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS,
7381 				null
7382 			);
7383 			if(hDirectory == INVALID_HANDLE_VALUE)
7384 				throw new SystemApiException("CreateFileW", GetLastError());
7385 
7386 			requestRead();
7387 		} else version(Arsd_core_epoll) {
7388 			auto el = getThisThreadEventLoop();
7389 
7390 			// no need for sync because it is thread-local
7391 			if(inotifyfd == -1) {
7392 				inotifyfd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
7393 				if(inotifyfd == -1)
7394 					throw new SystemApiException("inotify_init1", errno);
7395 
7396 				inotifycb = new CallbackHelper(&inotifyReady);
7397 				inotifyToken = el.addCallbackOnFdReadable(inotifyfd, inotifycb);
7398 			}
7399 
7400 			uint event_mask = IN_CREATE | IN_MODIFY  | IN_DELETE; // FIXME
7401 			CharzBuffer dtw = directoryToWatch.path;
7402 			auto watchId = inotify_add_watch(inotifyfd, dtw.ptr, event_mask);
7403 			if(watchId < -1)
7404 				throw new SystemApiException("inotify_add_watch", errno, [SavedArgument("path", LimitedVariant(directoryToWatch.path))]);
7405 
7406 			watchMappings[watchId] = this;
7407 
7408 			// FIXME: recursive needs to add child things individually
7409 
7410 		} else version(Arsd_core_kqueue) {
7411 			auto el = cast(CoreEventLoopImplementation) getThisThreadEventLoop();
7412 
7413 			// FIXME: need to scan for globPattern
7414 			// when a new file is added, i'll have to diff my list to detect it and open it too
7415 			// and recursive might need to scan down too.
7416 
7417 			kevent_t ev;
7418 
7419 			import core.sys.posix.fcntl;
7420 			CharzBuffer buffer = CharzBuffer(directoryToWatch.path);
7421 			fd = ErrnoEnforce!open(buffer.ptr, O_RDONLY);
7422 			setCloExec(fd);
7423 
7424 			cb = new CallbackHelper(&triggered);
7425 
7426 			EV_SET(&ev, fd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, NOTE_WRITE, 0, cast(void*) cb);
7427 			ErrnoEnforce!kevent(el.kqueuefd, &ev, 1, null, 0, null);
7428 		} else assert(0, "Not yet implemented for this platform");
7429 	}
7430 
7431 	private void triggered() {
7432 		writeln("triggered");
7433 	}
7434 
7435 	void dispose() {
7436 		version(Arsd_core_windows) {
7437 			CloseHandle(hDirectory);
7438 		} else version(Arsd_core_epoll) {
7439 			watchMappings.remove(watchId); // I could also do this on the IN_IGNORE notification but idk
7440 			inotify_rm_watch(inotifyfd, watchId);
7441 		} else version(Arsd_core_kqueue) {
7442 			ErrnoEnforce!close(fd);
7443 			fd = -1;
7444 		}
7445 	}
7446 }
7447 
7448 version(none)
7449 void main() {
7450 
7451 	// auto file = new AsyncFile(FilePath("test.txt"), AsyncFile.OpenMode.writeWithTruncation, AsyncFile.RequirePreexisting.yes);
7452 
7453 	/+
7454 	getFiles("c:/windows\\", (string filename, bool isDirectory) {
7455 		writeln(filename, " ", isDirectory ? "[dir]": "[file]");
7456 	});
7457 	+/
7458 
7459 	auto w = new DirectoryWatcher(FilePath("."), "*", false, (path, op) {
7460 		writeln(path.path);
7461 	});
7462 	getThisThreadEventLoop().run(() => false);
7463 }
7464 
7465 /++
7466 	This starts up a local pipe. If it is already claimed, it just communicates with the existing one through the interface.
7467 +/
7468 class SingleInstanceApplication {
7469 	// FIXME
7470 }
7471 
7472 version(none)
7473 void main() {
7474 
7475 	auto file = new AsyncFile(FilePath("test.txt"), AsyncFile.OpenMode.writeWithTruncation, AsyncFile.RequirePreexisting.yes);
7476 
7477 	auto buffer = cast(ubyte[]) "hello";
7478 	auto wr = new AsyncWriteRequest(file, buffer, 0);
7479 	wr.start();
7480 
7481 	wr.waitForCompletion();
7482 
7483 	file.close();
7484 }
7485 
7486 /++
7487 	Implementation details of some requests. You shouldn't need to know any of this, the interface is all public.
7488 +/
7489 mixin template OverlappedIoRequest(Response, LowLevelOperation) {
7490 	private {
7491 		LowLevelOperation llo;
7492 
7493 		OwnedClass!Response response;
7494 
7495 		version(Windows) {
7496 			OVERLAPPED overlapped;
7497 
7498 			extern(Windows)
7499 			static void overlappedCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransferred, LPOVERLAPPED lpOverlapped) @system {
7500 				typeof(this) rr = cast(typeof(this)) (cast(void*) lpOverlapped - typeof(this).overlapped.offsetof);
7501 
7502 				rr.response = typeof(rr.response)(SystemErrorCode(dwErrorCode), rr.llo.buffer[0 .. dwNumberOfBytesTransferred]);
7503 				rr.state_ = State.complete;
7504 
7505 				if(rr.oncomplete)
7506 					rr.oncomplete(rr);
7507 
7508 				// FIXME: on complete?
7509 				// this will queue our CallbackHelper and that should be run at the end of the event loop after it is woken up by the APC run
7510 			}
7511 		}
7512 
7513 		version(Posix) {
7514 			ICoreEventLoop.RearmToken eventRegistration;
7515 			CallbackHelper cb;
7516 
7517 			final CallbackHelper getCb() {
7518 				if(cb is null)
7519 					cb = new CallbackHelper(&cbImpl);
7520 				return cb;
7521 			}
7522 
7523 			final void cbImpl() {
7524 				// it is ready to complete, time to do it
7525 				auto ret = llo();
7526 				markCompleted(ret, errno);
7527 			}
7528 
7529 			void markCompleted(long ret, int errno) {
7530 				// maybe i should queue an apc to actually do it, to ensure the event loop has cycled... FIXME
7531 				if(ret == -1)
7532 					response = typeof(response)(SystemErrorCode(errno), null);
7533 				else
7534 					response = typeof(response)(SystemErrorCode(0), llo.buffer[0 .. cast(size_t) ret]);
7535 				state_ = State.complete;
7536 
7537 				if(oncomplete)
7538 					oncomplete(this);
7539 			}
7540 		}
7541 	}
7542 
7543 	enum State {
7544 		unused,
7545 		started,
7546 		inProgress,
7547 		complete
7548 	}
7549 	private State state_;
7550 
7551 	override void start() {
7552 		assert(state_ == State.unused);
7553 
7554 		state_ = State.started;
7555 
7556 		version(Windows) {
7557 			if(llo(&overlapped, &overlappedCompletionRoutine)) {
7558 				// all good, though GetLastError() might have some informative info
7559 				//writeln(GetLastError());
7560 			} else {
7561 				// operation failed, the operation is always ReadFileEx or WriteFileEx so it won't give the io pending thing here
7562 				// should i issue error async? idk
7563 				state_ = State.complete;
7564 				throw new SystemApiException(llo.errorString(), GetLastError());
7565 			}
7566 
7567 			// ReadFileEx always queues, even if it completed synchronously. I *could* check the get overlapped result and sleepex here but i'm prolly better off just letting the event loop do its thing anyway.
7568 		} else version(Posix) {
7569 
7570 			// first try to just do it
7571 			auto ret = llo();
7572 
7573 			auto errno = errno;
7574 			if(ret == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { // unable to complete right now, register and try when it is ready
7575 				if(eventRegistration is typeof(eventRegistration).init)
7576 					eventRegistration = getThisThreadEventLoop().addCallbackOnFdReadableOneShot(this.llo.file.handle, this.getCb);
7577 				else
7578 					eventRegistration.rearm();
7579 			} else {
7580 				// i could set errors sync or async and since it couldn't even start, i think a sync exception is the right way
7581 				if(ret == -1)
7582 					throw new SystemApiException(llo.errorString(), errno);
7583 				markCompleted(ret, errno); // it completed synchronously (if it is an error nor not is handled by the completion handler)
7584 			}
7585 		}
7586 	}
7587 
7588 	override void cancel() {
7589 		if(state_ == State.complete)
7590 			return; // it has already finished, just leave it alone, no point discarding what is already done
7591 		version(Windows) {
7592 			if(state_ != State.unused)
7593 				Win32Enforce!CancelIoEx(llo.file.AbstractFile.handle, &overlapped);
7594 			// Windows will notify us when the cancellation is complete, so we need to wait for that before updating the state
7595 		} else version(Posix) {
7596 			if(state_ != State.unused)
7597 				eventRegistration.unregister();
7598 			markCompleted(-1, ECANCELED);
7599 		}
7600 	}
7601 
7602 	override bool isComplete() {
7603 		// just always let the event loop do it instead
7604 		return state_ == State.complete;
7605 
7606 		/+
7607 		version(Windows) {
7608 			return HasOverlappedIoCompleted(&overlapped);
7609 		} else version(Posix) {
7610 			return state_ == State.complete;
7611 
7612 		}
7613 		+/
7614 	}
7615 
7616 	override Response waitForCompletion() {
7617 		if(state_ == State.unused)
7618 			start();
7619 
7620 		// FIXME: if we are inside a fiber, we can set a oncomplete callback and then yield instead...
7621 		if(state_ != State.complete)
7622 			getThisThreadEventLoop().run(&isComplete);
7623 
7624 		/+
7625 		version(Windows) {
7626 			SleepEx(INFINITE, true);
7627 
7628 			//DWORD numberTransferred;
7629 			//Win32Enforce!GetOverlappedResult(file.handle, &overlapped, &numberTransferred, true);
7630 		} else version(Posix) {
7631 			getThisThreadEventLoop().run(&isComplete);
7632 		}
7633 		+/
7634 
7635 		return response;
7636 	}
7637 
7638 	/++
7639 		Repeats the operation, restarting the request.
7640 
7641 		This must only be called when the operation has already completed.
7642 	+/
7643 	void repeat() {
7644 		if(state_ != State.complete)
7645 			throw new Exception("wrong use, cannot repeat if not complete");
7646 		state_ = State.unused;
7647 		start();
7648 	}
7649 
7650 	void delegate(typeof(this) t) oncomplete;
7651 }
7652 
7653 /++
7654 	You can write to a file asynchronously by creating one of these.
7655 +/
7656 version(HasSocket) final class AsyncWriteRequest : AsyncOperationRequest {
7657 	struct LowLevelOperation {
7658 		AsyncFile file;
7659 		ubyte[] buffer;
7660 		long offset;
7661 
7662 		this(typeof(this.tupleof) args) {
7663 			this.tupleof = args;
7664 		}
7665 
7666 		version(Windows) {
7667 			auto opCall(OVERLAPPED* overlapped, LPOVERLAPPED_COMPLETION_ROUTINE ocr) {
7668 				overlapped.Offset = (cast(ulong) offset) & 0xffff_ffff;
7669 				overlapped.OffsetHigh = ((cast(ulong) offset) >> 32) & 0xffff_ffff;
7670 				return WriteFileEx(file.handle, buffer.ptr, cast(int) buffer.length, overlapped, ocr);
7671 			}
7672 		} else {
7673 			auto opCall() {
7674 				return core.sys.posix.unistd.write(file.handle, buffer.ptr, buffer.length);
7675 			}
7676 		}
7677 
7678 		string errorString() {
7679 			return "Write";
7680 		}
7681 	}
7682 	mixin OverlappedIoRequest!(AsyncWriteResponse, LowLevelOperation);
7683 
7684 	this(AsyncFile file, ubyte[] buffer, long offset) {
7685 		this.llo = LowLevelOperation(file, buffer, offset);
7686 		response = typeof(response).defaultConstructed;
7687 	}
7688 }
7689 
7690 /++
7691 
7692 +/
7693 class AsyncWriteResponse : AsyncOperationResponse {
7694 	const ubyte[] bufferWritten;
7695 	const SystemErrorCode errorCode;
7696 
7697 	this(SystemErrorCode errorCode, const(ubyte)[] bufferWritten) {
7698 		this.errorCode = errorCode;
7699 		this.bufferWritten = bufferWritten;
7700 	}
7701 
7702 	override bool wasSuccessful() {
7703 		return errorCode.wasSuccessful;
7704 	}
7705 }
7706 
7707 // FIXME: on Windows, you may want two operations outstanding at once
7708 // so there's no delay between sequential ops. this system currently makes that
7709 // impossible since epoll won't let you register twice...
7710 
7711 // FIXME: if an op completes synchronously, and oncomplete calls repeat
7712 // you can get infinite recursion into the stack...
7713 
7714 /++
7715 
7716 +/
7717 version(HasSocket) final class AsyncReadRequest : AsyncOperationRequest {
7718 	struct LowLevelOperation {
7719 		AsyncFile file;
7720 		ubyte[] buffer;
7721 		long offset;
7722 
7723 		this(typeof(this.tupleof) args) {
7724 			this.tupleof = args;
7725 		}
7726 
7727 		version(Windows) {
7728 			auto opCall(OVERLAPPED* overlapped, LPOVERLAPPED_COMPLETION_ROUTINE ocr) {
7729 				overlapped.Offset = (cast(ulong) offset) & 0xffff_ffff;
7730 				overlapped.OffsetHigh = ((cast(ulong) offset) >> 32) & 0xffff_ffff;
7731 				return ReadFileEx(file.handle, buffer.ptr, cast(int) buffer.length, overlapped, ocr);
7732 			}
7733 		} else {
7734 			auto opCall() {
7735 				return core.sys.posix.unistd.read(file.handle, buffer.ptr, buffer.length);
7736 			}
7737 		}
7738 
7739 		string errorString() {
7740 			return "Read";
7741 		}
7742 	}
7743 	mixin OverlappedIoRequest!(AsyncReadResponse, LowLevelOperation);
7744 
7745 	/++
7746 		The file must have the overlapped flag enabled on Windows and the nonblock flag set on Posix.
7747 
7748 		The buffer MUST NOT be touched by you - not used by another request, modified, read, or freed, including letting a static array going out of scope - until this request's `isComplete` returns `true`.
7749 
7750 		The offset is where to start reading a disk file. For all other types of files, pass 0.
7751 	+/
7752 	this(AsyncFile file, ubyte[] buffer, long offset) {
7753 		this.llo = LowLevelOperation(file, buffer, offset);
7754 		response = typeof(response).defaultConstructed;
7755 	}
7756 
7757 	/++
7758 
7759 	+/
7760 	// abstract void repeat();
7761 }
7762 
7763 /++
7764 
7765 +/
7766 class AsyncReadResponse : AsyncOperationResponse {
7767 	const ubyte[] bufferRead;
7768 	const SystemErrorCode errorCode;
7769 
7770 	this(SystemErrorCode errorCode, const(ubyte)[] bufferRead) {
7771 		this.errorCode = errorCode;
7772 		this.bufferRead = bufferRead;
7773 	}
7774 
7775 	override bool wasSuccessful() {
7776 		return errorCode.wasSuccessful;
7777 	}
7778 }
7779 
7780 /+
7781 	Tasks:
7782 		startTask()
7783 		startSubTask() - what if it just did this when it knows it is being run from inside a task?
7784 		runHelperFunction() - whomever it reports to is the parent
7785 +/
7786 
7787 version(HasThread) class SchedulableTask : Fiber {
7788 	private void delegate() dg;
7789 
7790 	// linked list stuff
7791 	private static SchedulableTask taskRoot;
7792 	private SchedulableTask previous;
7793 	private SchedulableTask next;
7794 
7795 	// need the controlling thread to know how to wake it up if it receives a message
7796 	private Thread controllingThread;
7797 
7798 	// the api
7799 
7800 	this(void delegate() dg) {
7801 		assert(dg !is null);
7802 
7803 		this.dg = dg;
7804 		super(&taskRunner);
7805 
7806 		if(taskRoot !is null) {
7807 			this.next = taskRoot;
7808 			taskRoot.previous = this;
7809 		}
7810 		taskRoot = this;
7811 	}
7812 
7813 	/+
7814 	enum BehaviorOnCtrlC {
7815 		ignore,
7816 		cancel,
7817 		deliverMessage
7818 	}
7819 	+/
7820 
7821 	private bool cancelled;
7822 
7823 	public void cancel() {
7824 		this.cancelled = true;
7825 		// if this is running, we can throw immediately
7826 		// otherwise if we're calling from an appropriate thread, we can call it immediately
7827 		// otherwise we need to queue a wakeup to its own thread.
7828 		// tbh we should prolly just queue it every time
7829 	}
7830 
7831 	private void taskRunner() {
7832 		try {
7833 			dg();
7834 		} catch(TaskCancelledException tce) {
7835 			// this space intentionally left blank;
7836 			// the purpose of this exception is to just
7837 			// let the fiber's destructors run before we
7838 			// let it die.
7839 		} catch(Throwable t) {
7840 			if(taskUncaughtException is null) {
7841 				throw t;
7842 			} else {
7843 				taskUncaughtException(t);
7844 			}
7845 		} finally {
7846 			if(this is taskRoot) {
7847 				taskRoot = taskRoot.next;
7848 				if(taskRoot !is null)
7849 					taskRoot.previous = null;
7850 			} else {
7851 				assert(this.previous !is null);
7852 				assert(this.previous.next is this);
7853 				this.previous.next = this.next;
7854 				if(this.next !is null)
7855 					this.next.previous = this.previous;
7856 			}
7857 		}
7858 	}
7859 }
7860 
7861 /++
7862 
7863 +/
7864 void delegate(Throwable t) taskUncaughtException;
7865 
7866 /++
7867 	Gets an object that lets you control a schedulable task (which is a specialization of a fiber) and can be used in an `if` statement.
7868 
7869 	---
7870 		if(auto controller = inSchedulableTask()) {
7871 			controller.yieldUntilReadable(...);
7872 		}
7873 	---
7874 
7875 	History:
7876 		Added August 11, 2023 (dub v11.1)
7877 +/
7878 version(HasThread) SchedulableTaskController inSchedulableTask() {
7879 	import core.thread.fiber;
7880 
7881 	if(auto fiber = Fiber.getThis) {
7882 		return SchedulableTaskController(cast(SchedulableTask) fiber);
7883 	}
7884 
7885 	return SchedulableTaskController(null);
7886 }
7887 
7888 /// ditto
7889 version(HasThread) struct SchedulableTaskController {
7890 	private this(SchedulableTask fiber) {
7891 		this.fiber = fiber;
7892 	}
7893 
7894 	private SchedulableTask fiber;
7895 
7896 	/++
7897 
7898 	+/
7899 	bool opCast(T : bool)() {
7900 		return fiber !is null;
7901 	}
7902 
7903 	/++
7904 
7905 	+/
7906 	version(Posix)
7907 	void yieldUntilReadable(NativeFileHandle handle) {
7908 		assert(fiber !is null);
7909 
7910 		auto cb = new CallbackHelper(() { fiber.call(); });
7911 
7912 		// FIXME: if the fd is already registered in this thread it can throw...
7913 		version(Windows)
7914 			auto rearmToken = getThisThreadEventLoop().addCallbackOnFdReadableOneShot(handle, cb);
7915 		else
7916 			auto rearmToken = getThisThreadEventLoop().addCallbackOnFdReadableOneShot(handle, cb);
7917 
7918 		// FIXME: this is only valid if the fiber is only ever going to run in this thread!
7919 		fiber.yield();
7920 
7921 		rearmToken.unregister();
7922 
7923 		// what if there are other messages, like a ctrl+c?
7924 		if(fiber.cancelled)
7925 			throw new TaskCancelledException();
7926 	}
7927 
7928 	version(Windows)
7929 	void yieldUntilSignaled(NativeFileHandle handle) {
7930 		// add it to the WaitForMultipleObjects thing w/ a cb
7931 	}
7932 }
7933 
7934 class TaskCancelledException : object.Exception {
7935 	this() {
7936 		super("Task cancelled");
7937 	}
7938 }
7939 
7940 /+
7941 version(HasThread) private class ArsdThread : Thread {
7942 	this(void delegate() run) {
7943 		this.run = run;
7944 
7945 		super(&runner);
7946 	}
7947 
7948 	private void delegate() run;
7949 	final void runner() {
7950 		// FIXME: need to mask most signals so we don't handle things intended for the process as a whole
7951 
7952 		try {
7953 			run();
7954 
7955 			// FIXME: post a thread complete notification to the supervisor
7956 			// supervisor should join it at that time
7957 		} catch(Throwable t) {
7958 			// FIXME: post a thread failed notification to the supervisor
7959 			// supervisor should join it at that time
7960 			throw t;
7961 		}
7962 	}
7963 }
7964 +/
7965 
7966 version(HasThread) private class CoreWorkerThread : Thread {
7967 	this(EventLoopType type) {
7968 		this.type = type;
7969 
7970 		// task runners are supposed to have smallish stacks since they either just run a single callback or call into fibers
7971 		// the helper runners might be a bit bigger tho
7972 		super(&run);
7973 	}
7974 	void run() {
7975 		eventLoop = getThisThreadEventLoop(this.type);
7976 		atomicOp!"+="(startedCount, 1);
7977 		atomicOp!"+="(runningCount, 1);
7978 		scope(exit) {
7979 			atomicOp!"-="(runningCount, 1);
7980 		}
7981 
7982 		eventLoop.run(() => cancelled);
7983 	}
7984 
7985 	private bool cancelled;
7986 
7987 	void cancel() {
7988 		cancelled = true;
7989 	}
7990 
7991 	EventLoopType type;
7992 	ICoreEventLoop eventLoop;
7993 
7994 	__gshared static {
7995 		CoreWorkerThread[] taskRunners;
7996 		CoreWorkerThread[] helperRunners;
7997 		ICoreEventLoop mainThreadLoop;
7998 
7999 		// for the helper function thing on the bsds i could have my own little circular buffer of availability
8000 
8001 		shared(int) startedCount;
8002 		shared(int) runningCount;
8003 
8004 		bool started;
8005 
8006 		void setup(int numberOfTaskRunners, int numberOfHelpers) {
8007 			assert(!started);
8008 			synchronized {
8009 				mainThreadLoop = getThisThreadEventLoop();
8010 
8011 				foreach(i; 0 .. numberOfTaskRunners) {
8012 					auto nt = new CoreWorkerThread(EventLoopType.TaskRunner);
8013 					taskRunners ~= nt;
8014 					nt.start();
8015 				}
8016 				foreach(i; 0 .. numberOfHelpers) {
8017 					auto nt = new CoreWorkerThread(EventLoopType.HelperWorker);
8018 					helperRunners ~= nt;
8019 					nt.start();
8020 				}
8021 
8022 				const expectedCount = numberOfHelpers + numberOfTaskRunners;
8023 
8024 				while(startedCount < expectedCount) {
8025 					Thread.yield();
8026 				}
8027 
8028 				started = true;
8029 			}
8030 		}
8031 
8032 		void cancelAll() {
8033 			foreach(runner; taskRunners)
8034 				runner.cancel();
8035 			foreach(runner; helperRunners)
8036 				runner.cancel();
8037 
8038 		}
8039 	}
8040 }
8041 
8042 private int numberOfCpus() {
8043 	return 4; // FIXME
8044 }
8045 
8046 /++
8047 	To opt in to the full functionality of this module with customization opportunity, create one and only one of these objects that is valid for exactly the lifetime of the application.
8048 
8049 	Normally, this means writing a main like this:
8050 
8051 	---
8052 	import arsd.core;
8053 	void main() {
8054 		ArsdCoreApplication app = ArsdCoreApplication("Your app name");
8055 
8056 		// do your setup here
8057 
8058 		// the rest of your code here
8059 	}
8060 	---
8061 
8062 	Its destructor runs the event loop then waits to for the workers to finish to clean them up.
8063 +/
8064 // FIXME: single instance?
8065 version(HasThread) struct ArsdCoreApplication {
8066 	private ICoreEventLoop impl;
8067 
8068 	/++
8069 		default number of threads is to split your cpus between blocking function runners and task runners
8070 	+/
8071 	this(string applicationName) {
8072 		auto num = numberOfCpus();
8073 		num /= 2;
8074 		if(num <= 0)
8075 			num = 1;
8076 		this(applicationName, num, num);
8077 	}
8078 
8079 	/++
8080 
8081 	+/
8082 	this(string applicationName, int numberOfTaskRunners, int numberOfHelpers) {
8083 		impl = getThisThreadEventLoop(EventLoopType.Explicit);
8084 		CoreWorkerThread.setup(numberOfTaskRunners, numberOfHelpers);
8085 	}
8086 
8087 	@disable this();
8088 	@disable this(this);
8089 	/++
8090 		This must be deterministically destroyed.
8091 	+/
8092 	@disable new();
8093 
8094 	~this() {
8095 		if(!alreadyRun)
8096 			run();
8097 		exitApplication();
8098 		waitForWorkersToExit(3000);
8099 	}
8100 
8101 	void exitApplication() {
8102 		CoreWorkerThread.cancelAll();
8103 	}
8104 
8105 	void waitForWorkersToExit(int timeoutMilliseconds) {
8106 
8107 	}
8108 
8109 	private bool alreadyRun;
8110 
8111 	void run() {
8112 		impl.run(() => false);
8113 		alreadyRun = true;
8114 	}
8115 }
8116 
8117 
8118 private class CoreEventLoopImplementation : ICoreEventLoop {
8119 	version(EmptyEventLoop) RunOnceResult runOnce(Duration timeout = Duration.max) { return RunOnceResult(RunOnceResult.Possibilities.LocalExit); }
8120 	version(EmptyCoreEvent)
8121 	{
8122 		UnregisterToken addCallbackOnFdReadable(int fd, CallbackHelper cb){return typeof(return).init;}
8123 		RearmToken addCallbackOnFdReadableOneShot(int fd, CallbackHelper cb){return typeof(return).init;}
8124 		RearmToken addCallbackOnFdWritableOneShot(int fd, CallbackHelper cb){return typeof(return).init;}
8125 		private void rearmFd(RearmToken token) {}
8126 	}
8127 
8128 
8129 	private {
8130 		static struct LoopIterationDelegate {
8131 			void delegate() dg;
8132 			uint flags;
8133 		}
8134 		LoopIterationDelegate[] loopIterationDelegates;
8135 
8136 		void runLoopIterationDelegates(bool isAfter) {
8137 			foreach(lid; loopIterationDelegates)
8138 				if((!isAfter && (lid.flags & 1)) || (isAfter && (lid.flags & 2)))
8139 					lid.dg();
8140 		}
8141 	}
8142 
8143 	UnregisterToken addDelegateOnLoopIteration(void delegate() dg, uint timingFlags) {
8144 		loopIterationDelegates ~= LoopIterationDelegate(dg, timingFlags);
8145 		UnregisterToken ut;
8146 		ut.impl = this;
8147 		ut.dg = dg;
8148 		return ut;
8149 	}
8150 
8151 	void unregisterDg(void delegate() dg) {
8152 		LoopIterationDelegate[] toKeep;
8153 		foreach(lid; loopIterationDelegates) {
8154 			if(lid.dg !is dg) {
8155 				toKeep ~= lid;
8156 			}
8157 		}
8158 		loopIterationDelegates = toKeep;
8159 	}
8160 
8161 	version(Arsd_core_dispatch) {
8162 
8163 		private NSRunLoop ttrl;
8164 
8165 		private this() {
8166 			ttrl = NSRunLoop.currentRunLoop;
8167 		}
8168 
8169 			// FIXME: this lies!! it runs until completion
8170 		RunOnceResult runOnce(Duration timeout = Duration.max) {
8171 			scope(exit) eventLoopRound++;
8172 
8173 			// FIXME: autorelease pool
8174 
8175 			if(false /*isWorker*/) {
8176 				runLoopIterationDelegates(false);
8177 
8178 				// FIXME: timeout is wrong
8179 				auto retValue = ttrl.runMode(NSDefaultRunLoopMode, /+beforeDate:+/ NSDate.distantFuture);
8180 				if(retValue == false)
8181 					throw new Exception("could not start run loop");
8182 
8183 				runLoopIterationDelegates(true);
8184 
8185 				// NSApp.run();
8186 				// exitApplication();
8187 				//return RunOnceResult(RunOnceResult.Possibilities.GlobalExit);
8188 				return RunOnceResult(RunOnceResult.Possibilities.CarryOn);
8189 			} else {
8190 				// ui thread needs to pump nsapp events...
8191 				runLoopIterationDelegates(false);
8192 
8193 				auto timeoutNs = NSDate.distantFuture; // FIXME timeout here, future means no timeout
8194 
8195 				again:
8196 				NSEvent event = NSApp.nextEventMatchingMask(
8197 					NSEventMask.NSEventMaskAny,
8198 					timeoutNs,
8199 					NSDefaultRunLoopMode,
8200 					true
8201 				);
8202 				if(event !is null) {
8203 					NSApp.sendEvent(event);
8204 					timeoutNs = NSDate.distantPast; // only keep going if it won't block; we just want to clear the queue
8205 					goto again;
8206 				}
8207 
8208 				runLoopIterationDelegates(true);
8209 				return RunOnceResult(RunOnceResult.Possibilities.CarryOn);
8210 			}
8211 		}
8212 
8213 		UnregisterToken addCallbackOnFdReadable(int fd, CallbackHelper cb) {
8214 			auto input_src = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, dispatch_get_main_queue());
8215 			// FIXME: can the GC reap this prematurely?
8216 			auto b = block(() {
8217 				cb.call();
8218 			});
8219 			// FIXME: should prolly free it eventually idk
8220 			import core.memory;
8221 			GC.addRoot(b);
8222 
8223 			dispatch_source_set_event_handler(input_src, b);
8224 			// dispatch_source_set_cancel_handler(input_src,  ^{ close(my_file); });
8225 			dispatch_resume(input_src);
8226 
8227 			return UnregisterToken(this, fd, cb);
8228 
8229 		}
8230 		RearmToken addCallbackOnFdReadableOneShot(int fd, CallbackHelper cb) {
8231 			throw new NotYetImplementedException();
8232 		}
8233 		RearmToken addCallbackOnFdWritableOneShot(int fd, CallbackHelper cb) {
8234 			throw new NotYetImplementedException();
8235 		}
8236 		private void rearmFd(RearmToken token) {
8237 			if(token.readable)
8238 				cast(void) addCallbackOnFdReadableOneShot(token.fd, token.cb);
8239 			else
8240 				cast(void) addCallbackOnFdWritableOneShot(token.fd, token.cb);
8241 		}
8242 	}
8243 
8244 	version(Arsd_core_kqueue) {
8245 		// this thread apc dispatches go as a custom event to the queue
8246 		// the other queues go through one byte at a time pipes (barf). freebsd 13 and newest nbsd have eventfd too tho so maybe i can use them but the other kqueue systems don't.
8247 
8248 		RunOnceResult runOnce(Duration timeout = Duration.max) {
8249 			scope(exit) eventLoopRound++;
8250 
8251 			runLoopIterationDelegates(false);
8252 
8253 			kevent_t[16] ev;
8254 			//timespec tout = timespec(1, 0);
8255 			auto nev = kevent(kqueuefd, null, 0, ev.ptr, ev.length, null/*&tout*/);
8256 			if(nev == -1) {
8257 				// FIXME: EINTR
8258 				throw new SystemApiException("kevent", errno);
8259 			} else if(nev == 0) {
8260 				// timeout
8261 			} else {
8262 				foreach(event; ev[0 .. nev]) {
8263 					if(event.filter == EVFILT_SIGNAL) {
8264 						// FIXME: I could prolly do this better tbh
8265 						markSignalOccurred(cast(int) event.ident);
8266 						signalChecker();
8267 					} else {
8268 						// FIXME: event.filter more specific?
8269 						CallbackHelper cb = cast(CallbackHelper) event.udata;
8270 						cb.call();
8271 					}
8272 				}
8273 			}
8274 
8275 			runLoopIterationDelegates(true);
8276 
8277 			return RunOnceResult(RunOnceResult.Possibilities.CarryOn);
8278 		}
8279 
8280 		// FIXME: idk how to make one event that multiple kqueues can listen to w/o being shared
8281 		// maybe a shared kqueue could work that the thread kqueue listen to (which i rejected for
8282 		// epoll cuz it caused thundering herd problems but maybe it'd work here)
8283 
8284 		UnregisterToken addCallbackOnFdReadable(int fd, CallbackHelper cb) {
8285 			kevent_t ev;
8286 
8287 			EV_SET(&ev, fd, EVFILT_READ, EV_ADD | EV_ENABLE/* | EV_ONESHOT*/, 0, 0, cast(void*) cb);
8288 
8289 			ErrnoEnforce!kevent(kqueuefd, &ev, 1, null, 0, null);
8290 
8291 			return UnregisterToken(this, fd, cb);
8292 		}
8293 
8294 		RearmToken addCallbackOnFdReadableOneShot(int fd, CallbackHelper cb) {
8295 			kevent_t ev;
8296 
8297 			EV_SET(&ev, fd, EVFILT_READ, EV_ADD | EV_ENABLE/* | EV_ONESHOT*/, 0, 0, cast(void*) cb);
8298 
8299 			ErrnoEnforce!kevent(kqueuefd, &ev, 1, null, 0, null);
8300 
8301 			return RearmToken(true, this, fd, cb, 0);
8302 		}
8303 
8304 		RearmToken addCallbackOnFdWritableOneShot(int fd, CallbackHelper cb) {
8305 			kevent_t ev;
8306 
8307 			EV_SET(&ev, fd, EVFILT_WRITE, EV_ADD | EV_ENABLE/* | EV_ONESHOT*/, 0, 0, cast(void*) cb);
8308 
8309 			ErrnoEnforce!kevent(kqueuefd, &ev, 1, null, 0, null);
8310 
8311 			return RearmToken(false, this, fd, cb, 0);
8312 		}
8313 
8314 		private void rearmFd(RearmToken token) {
8315 			if(token.readable)
8316 				cast(void) addCallbackOnFdReadableOneShot(token.fd, token.cb);
8317 			else
8318 				cast(void) addCallbackOnFdWritableOneShot(token.fd, token.cb);
8319 		}
8320 
8321 		private void triggerGlobalEvent() {
8322 			ubyte a;
8323 			import core.sys.posix.unistd;
8324 			write(kqueueGlobalFd[1], &a, 1);
8325 		}
8326 
8327 		private this() {
8328 			kqueuefd = ErrnoEnforce!kqueue();
8329 			setCloExec(kqueuefd); // FIXME O_CLOEXEC
8330 
8331 			if(kqueueGlobalFd[0] == 0) {
8332 				import core.sys.posix.unistd;
8333 				pipe(kqueueGlobalFd);
8334 				setCloExec(kqueueGlobalFd[0]);
8335 				setCloExec(kqueueGlobalFd[1]);
8336 
8337 				signal(SIGINT, SIG_IGN); // FIXME
8338 			}
8339 
8340 			kevent_t ev;
8341 
8342 			EV_SET(&ev, SIGCHLD, EVFILT_SIGNAL, EV_ADD | EV_ENABLE, 0, 0, null);
8343 			ErrnoEnforce!kevent(kqueuefd, &ev, 1, null, 0, null);
8344 			EV_SET(&ev, SIGINT, EVFILT_SIGNAL, EV_ADD | EV_ENABLE, 0, 0, null);
8345 			ErrnoEnforce!kevent(kqueuefd, &ev, 1, null, 0, null);
8346 
8347 			globalEventSent = new CallbackHelper(&readGlobalEvent);
8348 			EV_SET(&ev, kqueueGlobalFd[0], EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, cast(void*) globalEventSent);
8349 			ErrnoEnforce!kevent(kqueuefd, &ev, 1, null, 0, null);
8350 		}
8351 
8352 		private int kqueuefd = -1;
8353 
8354 		private CallbackHelper globalEventSent;
8355 		void readGlobalEvent() {
8356 			kevent_t event;
8357 
8358 			import core.sys.posix.unistd;
8359 			ubyte a;
8360 			read(kqueueGlobalFd[0], &a, 1);
8361 
8362 			// FIXME: the thread is woken up, now we need to check the circualr buffer queue
8363 		}
8364 
8365 		private __gshared int[2] kqueueGlobalFd;
8366 	}
8367 
8368 	/+
8369 		// this setup  needs no extra allocation
8370 		auto op = read(file, buffer);
8371 		op.oncomplete = &thisfiber.call;
8372 		op.start();
8373 		thisfiber.yield();
8374 		auto result = op.waitForCompletion(); // guaranteed to return instantly thanks to previous setup
8375 
8376 		can generically abstract that into:
8377 
8378 		auto result = thisTask.await(read(file, buffer));
8379 
8380 
8381 		You MUST NOT use buffer in any way - not read, modify, deallocate, reuse, anything - until the PendingOperation is complete.
8382 
8383 		Note that PendingOperation may just be a wrapper around an internally allocated object reference... but then if you do a waitForFirstToComplete what happens?
8384 
8385 		those could of course just take the value type things
8386 	+/
8387 
8388 
8389 	version(Arsd_core_windows) {
8390 		// all event loops share the one iocp, Windows
8391 		// manages how to do it
8392 		__gshared HANDLE iocpTaskRunners;
8393 		__gshared HANDLE iocpWorkers;
8394 
8395 		HANDLE[] handles;
8396 		CallbackHelper[] handlesCbs;
8397 
8398 		void unregisterHandle(HANDLE handle, CallbackHelper cb) {
8399 			foreach(idx, h; handles)
8400 				if(h is handle && handlesCbs[idx] is cb) {
8401 					handles[idx] = handles[$-1];
8402 					handles = handles[0 .. $-1].assumeSafeAppend;
8403 
8404 					handlesCbs[idx] = handlesCbs[$-1];
8405 					handlesCbs = handlesCbs[0 .. $-1].assumeSafeAppend;
8406 				}
8407 		}
8408 
8409 		UnregisterToken addCallbackOnHandleReady(HANDLE handle, CallbackHelper cb) {
8410 			handles ~= handle;
8411 			handlesCbs ~= cb;
8412 
8413 			return UnregisterToken(this, handle, cb);
8414 		}
8415 
8416 		// i think to terminate i just have to post the message at least once for every thread i know about, maybe a few more times for threads i don't know about.
8417 
8418 		bool isWorker; // if it is a worker we wait on the iocp, if not we wait on msg
8419 
8420 		RunOnceResult runOnce(Duration timeout = Duration.max) {
8421 			scope(exit) eventLoopRound++;
8422 
8423 			runLoopIterationDelegates(false);
8424 
8425 			if(isWorker) {
8426 				// this function is only supported on Windows Vista and up, so using this
8427 				// means dropping support for XP.
8428 				//GetQueuedCompletionStatusEx();
8429 				assert(0); // FIXME
8430 			} else {
8431 				auto wto = getTimeout();
8432 
8433 				auto waitResult = MsgWaitForMultipleObjectsEx(
8434 					cast(int) handles.length, handles.ptr,
8435 					(wto == 0 ? INFINITE : wto), /* timeout */
8436 					0x04FF, /* QS_ALLINPUT */
8437 					0x0002 /* MWMO_ALERTABLE */ | 0x0004 /* MWMO_INPUTAVAILABLE */);
8438 
8439 				enum WAIT_OBJECT_0 = 0;
8440 				if(waitResult >= WAIT_OBJECT_0 && waitResult < handles.length + WAIT_OBJECT_0) {
8441 					auto h = handles[waitResult - WAIT_OBJECT_0];
8442 					auto cb = handlesCbs[waitResult - WAIT_OBJECT_0];
8443 					cb.call();
8444 				} else if(waitResult == handles.length + WAIT_OBJECT_0) {
8445 					// message ready
8446 					int count;
8447 					MSG message;
8448 					while(PeekMessage(&message, null, 0, 0, PM_NOREMOVE)) { // need to peek since sometimes MsgWaitForMultipleObjectsEx returns even though GetMessage can block. tbh i don't fully understand it but the docs say it is foreground activation
8449 						auto ret = GetMessage(&message, null, 0, 0);
8450 						if(ret == -1)
8451 							throw new WindowsApiException("GetMessage", GetLastError());
8452 						TranslateMessage(&message);
8453 						DispatchMessage(&message);
8454 
8455 						count++;
8456 						if(count > 10)
8457 							break; // take the opportunity to catch up on other events
8458 
8459 						if(ret == 0) { // WM_QUIT
8460 							exitApplication();
8461 						}
8462 					}
8463 				} else if(waitResult == 0x000000C0L /* WAIT_IO_COMPLETION */) {
8464 					SleepEx(0, true); // I call this to give it a chance to do stuff like async io
8465 				} else if(waitResult == 258L /* WAIT_TIMEOUT */) {
8466 					// timeout, should never happen since we aren't using it
8467 				} else if(waitResult == 0xFFFFFFFF) {
8468 						// failed
8469 						throw new WindowsApiException("MsgWaitForMultipleObjectsEx", GetLastError());
8470 				} else {
8471 					// idk....
8472 				}
8473 			}
8474 
8475 			runLoopIterationDelegates(true);
8476 
8477 			return RunOnceResult(RunOnceResult.Possibilities.CarryOn);
8478 		}
8479 	}
8480 
8481 	version(Posix) {
8482 		private __gshared uint sigChildHappened = 0;
8483 		private __gshared uint sigIntrHappened = 0;
8484 
8485 		static void signalChecker() {
8486 			if(cas(&sigChildHappened, 1, 0)) {
8487 				while(true) { // multiple children could have exited before we processed the notification
8488 
8489 					// Means child stopped, terminated, or continued. Not necessarily just terminated!
8490 
8491 					import core.sys.posix.sys.wait;
8492 
8493 					int status;
8494 					auto pid = waitpid(-1, &status, WNOHANG | WUNTRACED | WCONTINUED);
8495 					if(pid == -1) {
8496 						import core.stdc.errno;
8497 						auto errno = errno;
8498 						if(errno == ECHILD)
8499 							break; // also all done, there are no children left
8500 						// no need to check EINTR since we set WNOHANG
8501 						throw new ErrnoApiException("waitpid", errno);
8502 					}
8503 					if(pid == 0)
8504 						break; // all done, all children are still running
8505 
8506 					// look up the pid for one of our objects
8507 					// if it is found, inform it of its status
8508 					// and then inform its controlling thread
8509 					// to wake up so it can check its waitForCompletion,
8510 					// trigger its callbacks, etc.
8511 
8512 					ExternalProcess.recordChildChanged(pid, status);
8513 				}
8514 
8515 			}
8516 			if(cas(&sigIntrHappened, 1, 0)) {
8517 				// FIXME
8518 				import core.stdc.stdlib;
8519 				exit(0);
8520 			}
8521 		}
8522 
8523 		/++
8524 			Informs the arsd.core system that the given signal happened. You can call this from inside a signal handler.
8525 		+/
8526 		public static void markSignalOccurred(int sigNumber) nothrow {
8527 			import core.sys.posix.unistd;
8528 
8529 			if(sigNumber == SIGCHLD)
8530 				volatileStore(&sigChildHappened, 1);
8531 			if(sigNumber == SIGINT)
8532 				volatileStore(&sigIntrHappened, 1);
8533 
8534 			version(Arsd_core_epoll) {
8535 				ulong writeValue = 1;
8536 				write(signalPipeFd, &writeValue, writeValue.sizeof);
8537 			}
8538 		}
8539 	}
8540 
8541 	version(Arsd_core_epoll) {
8542 
8543 		import core.sys.linux.epoll;
8544 		import core.sys.linux.sys.eventfd;
8545 
8546 		private this() {
8547 
8548 			if(!globalsInitialized) {
8549 				synchronized {
8550 					if(!globalsInitialized) {
8551 						// blocking signals is problematic because it is inherited by child processes
8552 						// and that can be problematic for general purpose stuff so i use a self pipe
8553 						// here. though since it is linux, im using an eventfd instead just to notify
8554 						signalPipeFd = ErrnoEnforce!eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
8555 						signalReaderCallback = new CallbackHelper(&signalReader);
8556 
8557 						runInTaskRunnerQueue = new CallbackQueue("task runners", true);
8558 						runInHelperThreadQueue = new CallbackQueue("helper threads", true);
8559 
8560 						setSignalHandlers();
8561 
8562 						globalsInitialized = true;
8563 					}
8564 				}
8565 			}
8566 
8567 			epollfd = epoll_create1(EPOLL_CLOEXEC);
8568 
8569 			// FIXME: ensure UI events get top priority
8570 
8571 			// global listeners
8572 
8573 			// FIXME: i should prolly keep the tokens and release them when tearing down.
8574 
8575 			cast(void) addCallbackOnFdReadable(signalPipeFd, signalReaderCallback);
8576 			if(true) { // FIXME: if this is a task runner vs helper thread vs ui thread
8577 				cast(void) addCallbackOnFdReadable(runInTaskRunnerQueue.fd, runInTaskRunnerQueue.callback);
8578 				runInTaskRunnerQueue.callback.addref();
8579 			} else {
8580 				cast(void) addCallbackOnFdReadable(runInHelperThreadQueue.fd, runInHelperThreadQueue.callback);
8581 				runInHelperThreadQueue.callback.addref();
8582 			}
8583 
8584 			// local listener
8585 			thisThreadQueue = new CallbackQueue("this thread", false);
8586 			cast(void) addCallbackOnFdReadable(thisThreadQueue.fd, thisThreadQueue.callback);
8587 
8588 			// what are we going to do about timers?
8589 		}
8590 
8591 		void teardown() {
8592 			import core.sys.posix.fcntl;
8593 			import core.sys.posix.unistd;
8594 
8595 			close(epollfd);
8596 			epollfd = -1;
8597 
8598 			thisThreadQueue.teardown();
8599 
8600 			// FIXME: should prolly free anything left in the callback queue, tho those could also be GC managed tbh.
8601 		}
8602 
8603 		/+ // i call it explicitly at the thread exit instead, but worker threads aren't really supposed to exit generally speaking till process done anyway
8604 		static ~this() {
8605 			teardown();
8606 		}
8607 		+/
8608 
8609 		static void teardownGlobals() {
8610 			import core.sys.posix.fcntl;
8611 			import core.sys.posix.unistd;
8612 
8613 			synchronized {
8614 				restoreSignalHandlers();
8615 				close(signalPipeFd);
8616 				signalReaderCallback.release();
8617 
8618 				runInTaskRunnerQueue.teardown();
8619 				runInHelperThreadQueue.teardown();
8620 
8621 				globalsInitialized = false;
8622 			}
8623 
8624 		}
8625 
8626 
8627 		private static final class CallbackQueue : SynchronizableObject {
8628 			int fd = -1;
8629 			string name;
8630 			CallbackHelper callback;
8631 			SynchronizedCircularBuffer!CallbackHelper queue;
8632 
8633 			this(string name, bool dequeueIsShared) {
8634 				this.name = name;
8635 				queue = typeof(queue)(this);
8636 
8637 				fd = ErrnoEnforce!eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK | (dequeueIsShared ? EFD_SEMAPHORE : 0));
8638 
8639 				callback = new CallbackHelper(dequeueIsShared ? &sharedDequeueCb : &threadLocalDequeueCb);
8640 			}
8641 
8642 			bool resetEvent() {
8643 				import core.sys.posix.unistd;
8644 				ulong count;
8645 				return read(fd, &count, count.sizeof) == count.sizeof;
8646 			}
8647 
8648 			void sharedDequeueCb() {
8649 				if(resetEvent()) {
8650 					auto cb = queue.dequeue();
8651 					cb.call();
8652 					cb.release();
8653 				}
8654 			}
8655 
8656 			void threadLocalDequeueCb() {
8657 				CallbackHelper[16] buffer;
8658 				foreach(cb; queue.dequeueSeveral(buffer[], () { resetEvent(); })) {
8659 					cb.call();
8660 					cb.release();
8661 				}
8662 			}
8663 
8664 			void enqueue(CallbackHelper cb) {
8665 				if(queue.enqueue(cb)) {
8666 					import core.sys.posix.unistd;
8667 					ulong count = 1;
8668 					ErrnoEnforce!write(fd, &count, count.sizeof);
8669 				} else {
8670 					throw new ArsdException!"queue is full"(name);
8671 				}
8672 			}
8673 
8674 			void teardown() {
8675 				import core.sys.posix.fcntl;
8676 				import core.sys.posix.unistd;
8677 
8678 				close(fd);
8679 				fd = -1;
8680 
8681 				callback.release();
8682 			}
8683 		}
8684 
8685 		// there's a global instance of this we refer back to
8686 		private __gshared {
8687 			bool globalsInitialized;
8688 
8689 			CallbackHelper signalReaderCallback;
8690 
8691 			CallbackQueue runInTaskRunnerQueue;
8692 			CallbackQueue runInHelperThreadQueue;
8693 
8694 			int exitEventFd = -1; // FIXME: implement
8695 		}
8696 
8697 		// and then the local loop
8698 		private {
8699 			int epollfd = -1;
8700 
8701 			CallbackQueue thisThreadQueue;
8702 		}
8703 
8704 		// signal stuff {
8705 		import core.sys.posix.signal;
8706 
8707 		private __gshared sigaction_t oldSigIntr;
8708 		private __gshared sigaction_t oldSigChld;
8709 		private __gshared sigaction_t oldSigPipe;
8710 
8711 		private __gshared int signalPipeFd = -1;
8712 		// sigpipe not important, i handle errors on the writes
8713 
8714 		public static void setSignalHandlers() {
8715 			static extern(C) void interruptHandler(int sigNumber) nothrow {
8716 				markSignalOccurred(sigNumber);
8717 
8718 				/+
8719 				// calling the old handler is non-trivial since there can be ignore
8720 				// or default or a plain handler or a sigaction 3 arg handler and i
8721 				// i don't think it is worth teh complication
8722 				sigaction_t* oldHandler;
8723 				if(sigNumber == SIGCHLD)
8724 					oldHandler = &oldSigChld;
8725 				else if(sigNumber == SIGINT)
8726 					oldHandler = &oldSigIntr;
8727 				if(oldHandler && oldHandler.sa_handler)
8728 					oldHandler
8729 				+/
8730 			}
8731 
8732 			sigaction_t n;
8733 			n.sa_handler = &interruptHandler;
8734 			n.sa_mask = cast(sigset_t) 0;
8735 			n.sa_flags = 0;
8736 			sigaction(SIGINT, &n, &oldSigIntr);
8737 			sigaction(SIGCHLD, &n, &oldSigChld);
8738 
8739 			n.sa_handler = SIG_IGN;
8740 			sigaction(SIGPIPE, &n, &oldSigPipe);
8741 		}
8742 
8743 		public static void restoreSignalHandlers() {
8744 			sigaction(SIGINT, &oldSigIntr, null);
8745 			sigaction(SIGCHLD, &oldSigChld, null);
8746 			sigaction(SIGPIPE, &oldSigPipe, null);
8747 		}
8748 
8749 		private static void signalReader() {
8750 			import core.sys.posix.unistd;
8751 			ulong number;
8752 			read(signalPipeFd, &number, number.sizeof);
8753 
8754 			signalChecker();
8755 		}
8756 		// signal stuff done }
8757 
8758 		// the any thread poll is just registered in the this thread poll w/ exclusive. nobody actaully epoll_waits
8759 		// on the global one directly.
8760 
8761 		RunOnceResult runOnce(Duration timeout = Duration.max) {
8762 			scope(exit) eventLoopRound++;
8763 
8764 			runLoopIterationDelegates(false);
8765 
8766 			epoll_event[16] events;
8767 			auto ret = epoll_wait(epollfd, events.ptr, cast(int) events.length, getTimeout ? getTimeout() : -1); // FIXME: timeout argument
8768 			if(ret == -1) {
8769 				import core.stdc.errno;
8770 				if(errno == EINTR) {
8771 					return RunOnceResult(RunOnceResult.Possibilities.Interrupted);
8772 				}
8773 				throw new ErrnoApiException("epoll_wait", errno);
8774 			} else if(ret == 0) {
8775 				// timeout
8776 			} else {
8777 				// loop events and call associated callbacks
8778 				foreach(event; events[0 .. ret]) {
8779 					auto flags = event.events;
8780 					auto cbObject = cast(CallbackHelper) event.data.ptr;
8781 
8782 					// FIXME: or if it is an error...
8783 					// EPOLLERR - write end of pipe when read end closed or other error. and EPOLLHUP - terminal hangup or read end when write end close (but it will give 0 reading after that soon anyway)
8784 
8785 					cbObject.call();
8786 				}
8787 			}
8788 
8789 			runLoopIterationDelegates(true);
8790 
8791 			return RunOnceResult(RunOnceResult.Possibilities.CarryOn);
8792 		}
8793 
8794 		// building blocks for low-level integration with the loop
8795 
8796 		UnregisterToken addCallbackOnFdReadable(int fd, CallbackHelper cb) {
8797 			epoll_event event;
8798 			event.data.ptr = cast(void*) cb;
8799 			event.events = EPOLLIN | EPOLLEXCLUSIVE;
8800 			if(epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event) == -1)
8801 				throw new ErrnoApiException("epoll_ctl", errno);
8802 
8803 			return UnregisterToken(this, fd, cb);
8804 		}
8805 
8806 		/++
8807 			Adds a one-off callback that you can optionally rearm when it happens.
8808 		+/
8809 		RearmToken addCallbackOnFdReadableOneShot(int fd, CallbackHelper cb) {
8810 			epoll_event event;
8811 			event.data.ptr = cast(void*) cb;
8812 			event.events = EPOLLIN | EPOLLONESHOT;
8813 			if(epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event) == -1)
8814 				throw new ErrnoApiException("epoll_ctl", errno);
8815 
8816 			return RearmToken(true, this, fd, cb, EPOLLIN | EPOLLONESHOT);
8817 		}
8818 
8819 		/++
8820 			Adds a one-off callback that you can optionally rearm when it happens.
8821 		+/
8822 		RearmToken addCallbackOnFdWritableOneShot(int fd, CallbackHelper cb) {
8823 			epoll_event event;
8824 			event.data.ptr = cast(void*) cb;
8825 			event.events = EPOLLOUT | EPOLLONESHOT;
8826 			if(epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event) == -1)
8827 				throw new ErrnoApiException("epoll_ctl", errno);
8828 
8829 			return RearmToken(false, this, fd, cb, EPOLLOUT | EPOLLONESHOT);
8830 		}
8831 
8832 		private void unregisterFd(int fd) {
8833 			epoll_event event;
8834 			if(epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, &event) == -1)
8835 				throw new ErrnoApiException("epoll_ctl", errno);
8836 		}
8837 
8838 		private void rearmFd(RearmToken token) {
8839 			epoll_event event;
8840 			event.data.ptr = cast(void*) token.cb;
8841 			event.events = token.flags;
8842 			if(epoll_ctl(epollfd, EPOLL_CTL_MOD, token.fd, &event) == -1)
8843 				throw new ErrnoApiException("epoll_ctl", errno);
8844 		}
8845 
8846 		// Disk files will have to be sent as messages to a worker to do the read and report back a completion packet.
8847 	}
8848 
8849 	version(Arsd_core_kqueue) {
8850 		// FIXME
8851 	}
8852 
8853 	// cross platform adapters
8854 	void setTimeout() {}
8855 	void addFileOrDirectoryChangeListener(FilePath name, uint flags, bool recursive = false) {}
8856 }
8857 
8858 // deduplication???????//
8859 bool postMessage(ThreadToRunIn destination, void delegate() code) {
8860 	return false;
8861 }
8862 bool postMessage(ThreadToRunIn destination, Object message) {
8863 	return false;
8864 }
8865 
8866 /+
8867 void main() {
8868 	// FIXME: the offset doesn't seem to be done right
8869 	auto file = new AsyncFile(FilePath("test.txt"), AsyncFile.OpenMode.writeWithTruncation);
8870 	file.write("hello", 10).waitForCompletion();
8871 }
8872 +/
8873 
8874 // to test the mailboxes
8875 /+
8876 void main() {
8877 	/+
8878 	import std.stdio;
8879 	Thread[4] pool;
8880 
8881 	bool shouldExit;
8882 
8883 	static int received;
8884 
8885 	static void tester() {
8886 		received++;
8887 		//writeln(cast(void*) Thread.getThis, " ", received);
8888 	}
8889 
8890 	foreach(ref thread; pool) {
8891 		thread = new Thread(() {
8892 			getThisThreadEventLoop().run(() {
8893 				return shouldExit;
8894 			});
8895 		});
8896 		thread.start();
8897 	}
8898 
8899 	getThisThreadEventLoop(); // ensure it is all initialized before proceeding. FIXME: i should have an ensure initialized function i do on most the public apis.
8900 
8901 	int lol;
8902 
8903 	try
8904 	foreach(i; 0 .. 6000) {
8905 		CoreEventLoopImplementation.runInTaskRunnerQueue.enqueue(new CallbackHelper(&tester));
8906 		lol = cast(int) i;
8907 	}
8908 	catch(ArsdExceptionBase e)  {
8909 		Thread.sleep(50.msecs);
8910 		writeln(e);
8911 		writeln(lol);
8912 	}
8913 
8914 	import core.stdc.stdlib;
8915 	exit(0);
8916 
8917 	version(none)
8918 	foreach(i; 0 .. 100)
8919 		CoreEventLoopImplementation.runInTaskRunnerQueue.enqueue(new CallbackHelper(&tester));
8920 
8921 
8922 	foreach(ref thread; pool) {
8923 		thread.join();
8924 	}
8925 	+/
8926 
8927 
8928 	static int received;
8929 
8930 	static void tester() {
8931 		received++;
8932 		//writeln(cast(void*) Thread.getThis, " ", received);
8933 	}
8934 
8935 
8936 
8937 	auto ev = cast(CoreEventLoopImplementation) getThisThreadEventLoop();
8938 	foreach(i; 0 .. 100)
8939 		ev.thisThreadQueue.enqueue(new CallbackHelper(&tester));
8940 	foreach(i; 0 .. 100 / 16 + 1)
8941 	ev.runOnce();
8942 	import std.conv;
8943 	assert(received == 100, to!string(received));
8944 
8945 }
8946 +/
8947 
8948 /++
8949 	This is primarily a helper for the event queues. It is public in the hope it might be useful,
8950 	but subject to change without notice; I will treat breaking it the same as if it is private.
8951 	(That said, it is a simple little utility that does its job, so it is unlikely to change much.
8952 	The biggest change would probably be letting it grow and changing from inline to dynamic array.)
8953 
8954 	It is a fixed-size ring buffer that synchronizes on a given object you give it in the constructor.
8955 
8956 	After enqueuing something, you should probably set an event to notify the other threads. This is left
8957 	as an exercise to you (or another wrapper).
8958 +/
8959 struct SynchronizedCircularBuffer(T, size_t maxSize = 128) {
8960 	private T[maxSize] ring;
8961 	private int front;
8962 	private int back;
8963 
8964 	private SynchronizableObject synchronizedOn;
8965 
8966 	@disable this();
8967 
8968 	/++
8969 		The Object's monitor is used to synchronize the methods in here.
8970 	+/
8971 	this(SynchronizableObject synchronizedOn) {
8972 		this.synchronizedOn = synchronizedOn;
8973 	}
8974 
8975 	/++
8976 		Note the potential race condition between calling this and actually dequeuing something. You might
8977 		want to acquire the lock on the object before calling this (nested synchronized things are allowed
8978 		as long as the same thread is the one doing it).
8979 	+/
8980 	bool isEmpty() {
8981 		synchronized(this.synchronizedOn) {
8982 			return front == back;
8983 		}
8984 	}
8985 
8986 	/++
8987 		Note the potential race condition between calling this and actually queuing something.
8988 	+/
8989 	bool isFull() {
8990 		synchronized(this.synchronizedOn) {
8991 			return isFullUnsynchronized();
8992 		}
8993 	}
8994 
8995 	private bool isFullUnsynchronized() nothrow const {
8996 		return ((back + 1) % ring.length) == front;
8997 
8998 	}
8999 
9000 	/++
9001 		If this returns true, you should signal listening threads (with an event or a semaphore,
9002 		depending on how you dequeue it). If it returns false, the queue was full and your thing
9003 		was NOT added. You might wait and retry later (you could set up another event to signal it
9004 		has been read and wait for that, or maybe try on a timer), or just fail and throw an exception
9005 		or to abandon the message.
9006 	+/
9007 	bool enqueue(T what) {
9008 		synchronized(this.synchronizedOn) {
9009 			if(isFullUnsynchronized())
9010 				return false;
9011 			ring[(back++) % ring.length] = what;
9012 			return true;
9013 		}
9014 	}
9015 
9016 	private T dequeueUnsynchronized() nothrow {
9017 		assert(front != back);
9018 		return ring[(front++) % ring.length];
9019 	}
9020 
9021 	/++
9022 		If you are using a semaphore to signal, you can call this once for each count of it
9023 		and you can do that separately from this call (though they should be paired).
9024 
9025 		If you are using an event, you should use [dequeueSeveral] instead to drain it.
9026 	+/
9027 	T dequeue() {
9028 		synchronized(this.synchronizedOn) {
9029 			return dequeueUnsynchronized();
9030 		}
9031 	}
9032 
9033 	/++
9034 		Note that if you use a semaphore to signal waiting threads, you should probably not call this.
9035 
9036 		If you use a set/reset event, there's a potential race condition between the dequeue and event
9037 		reset. This is why the `runInsideLockIfEmpty` delegate is there - when it is empty, before it
9038 		unlocks, it will give you a chance to reset the event. Otherwise, it can remain set to indicate
9039 		that there's still pending data in the queue.
9040 	+/
9041 	T[] dequeueSeveral(return T[] buffer, scope void delegate() runInsideLockIfEmpty = null) {
9042 		int pos;
9043 		synchronized(this.synchronizedOn) {
9044 			while(pos < buffer.length && front != back) {
9045 				buffer[pos++] = dequeueUnsynchronized();
9046 			}
9047 			if(front == back && runInsideLockIfEmpty !is null)
9048 				runInsideLockIfEmpty();
9049 		}
9050 		return buffer[0 .. pos];
9051 	}
9052 }
9053 
9054 unittest {
9055 	SynchronizableObject object = new SynchronizableObject();
9056 	auto queue = SynchronizedCircularBuffer!CallbackHelper(object);
9057 	assert(queue.isEmpty);
9058 	foreach(i; 0 .. queue.ring.length - 1)
9059 		queue.enqueue(cast(CallbackHelper) cast(void*) i);
9060 	assert(queue.isFull);
9061 
9062 	foreach(i; 0 .. queue.ring.length - 1)
9063 		assert(queue.dequeue() is (cast(CallbackHelper) cast(void*) i));
9064 	assert(queue.isEmpty);
9065 
9066 	foreach(i; 0 .. queue.ring.length - 1)
9067 		queue.enqueue(cast(CallbackHelper) cast(void*) i);
9068 	assert(queue.isFull);
9069 
9070 	CallbackHelper[] buffer = new CallbackHelper[](300);
9071 	auto got = queue.dequeueSeveral(buffer);
9072 	assert(got.length == queue.ring.length - 1);
9073 	assert(queue.isEmpty);
9074 	foreach(i, item; got)
9075 		assert(item is (cast(CallbackHelper) cast(void*) i));
9076 
9077 	foreach(i; 0 .. 8)
9078 		queue.enqueue(cast(CallbackHelper) cast(void*) i);
9079 	buffer = new CallbackHelper[](4);
9080 	got = queue.dequeueSeveral(buffer);
9081 	assert(got.length == 4);
9082 	foreach(i, item; got)
9083 		assert(item is (cast(CallbackHelper) cast(void*) i));
9084 	got = queue.dequeueSeveral(buffer);
9085 	assert(got.length == 4);
9086 	foreach(i, item; got)
9087 		assert(item is (cast(CallbackHelper) cast(void*) (i+4)));
9088 	got = queue.dequeueSeveral(buffer);
9089 	assert(got.length == 0);
9090 	assert(queue.isEmpty);
9091 }
9092 
9093 /++
9094 
9095 +/
9096 enum ByteOrder {
9097 	irrelevant,
9098 	littleEndian,
9099 	bigEndian,
9100 }
9101 
9102 /++
9103 	A class to help write a stream of binary data to some target.
9104 
9105 	NOT YET FUNCTIONAL
9106 +/
9107 class WritableStream {
9108 	/++
9109 
9110 	+/
9111 	this(size_t bufferSize) {
9112 		this(new ubyte[](bufferSize));
9113 	}
9114 
9115 	/// ditto
9116 	this(ubyte[] buffer) {
9117 		this.buffer = buffer;
9118 	}
9119 
9120 	/++
9121 
9122 	+/
9123 	final void put(T)(T value, ByteOrder byteOrder = ByteOrder.irrelevant, string file = __FILE__, size_t line = __LINE__) {
9124 		static if(T.sizeof == 8)
9125 			ulong b;
9126 		else static if(T.sizeof == 4)
9127 			uint b;
9128 		else static if(T.sizeof == 2)
9129 			ushort b;
9130 		else static if(T.sizeof == 1)
9131 			ubyte b;
9132 		else static assert(0, "unimplemented type, try using just the basic types");
9133 
9134 		if(byteOrder == ByteOrder.irrelevant && T.sizeof > 1)
9135 			throw new InvalidArgumentsException("byteOrder", "byte order must be specified for type " ~ T.stringof ~ " because it is bigger than one byte", "WritableStream.put", file, line);
9136 
9137 		final switch(byteOrder) {
9138 			case ByteOrder.irrelevant:
9139 				writeOneByte(b);
9140 			break;
9141 			case ByteOrder.littleEndian:
9142 				foreach(i; 0 .. T.sizeof) {
9143 					writeOneByte(b & 0xff);
9144 					b >>= 8;
9145 				}
9146 			break;
9147 			case ByteOrder.bigEndian:
9148 				int amount = T.sizeof * 8 - 8;
9149 				foreach(i; 0 .. T.sizeof) {
9150 					writeOneByte((b >> amount) & 0xff);
9151 					amount -= 8;
9152 				}
9153 			break;
9154 		}
9155 	}
9156 
9157 	/// ditto
9158 	final void put(T : E[], E)(T value, ByteOrder elementByteOrder = ByteOrder.irrelevant, string file = __FILE__, size_t line = __LINE__) {
9159 		foreach(item; value)
9160 			put(item, elementByteOrder, file, line);
9161 	}
9162 
9163 	/++
9164 		Performs a final flush() call, then marks the stream as closed, meaning no further data will be written to it.
9165 	+/
9166 	void close() {
9167 		isClosed_ = true;
9168 	}
9169 
9170 	/++
9171 		Writes what is currently in the buffer to the target and waits for the target to accept it.
9172 		Please note: if you are subclassing this to go to a different target
9173 	+/
9174 	void flush() {}
9175 
9176 	/++
9177 		Returns true if either you closed it or if the receiving end closed their side, indicating they
9178 		don't want any more data.
9179 	+/
9180 	bool isClosed() {
9181 		return isClosed_;
9182 	}
9183 
9184 	// hasRoomInBuffer
9185 	// canFlush
9186 	// waitUntilCanFlush
9187 
9188 	// flushImpl
9189 	// markFinished / close - tells the other end you're done
9190 
9191 	private final writeOneByte(ubyte value) {
9192 		if(bufferPosition == buffer.length)
9193 			flush();
9194 
9195 		buffer[bufferPosition++] = value;
9196 	}
9197 
9198 
9199 	private {
9200 		ubyte[] buffer;
9201 		int bufferPosition;
9202 		bool isClosed_;
9203 	}
9204 }
9205 
9206 /++
9207 	A stream can be used by just one task at a time, but one task can consume multiple streams.
9208 
9209 	Streams may be populated by async sources (in which case they must be called from a fiber task),
9210 	from a function generating the data on demand (including an input range), from memory, or from a synchronous file.
9211 
9212 	A stream of heterogeneous types is compatible with input ranges.
9213 
9214 	It reads binary data.
9215 +/
9216 version(HasThread) class ReadableStream {
9217 
9218 	this() {
9219 
9220 	}
9221 
9222 	/++
9223 		Gets data of the specified type `T` off the stream. The byte order of the T on the stream must be specified unless it is irrelevant (e.g. single byte entries).
9224 
9225 		---
9226 		// get an int out of a big endian stream
9227 		int i = stream.get!int(ByteOrder.bigEndian);
9228 
9229 		// get i bytes off the stream
9230 		ubyte[] data = stream.get!(ubyte[])(i);
9231 		---
9232 	+/
9233 	final T get(T)(ByteOrder byteOrder = ByteOrder.irrelevant, string file = __FILE__, size_t line = __LINE__) {
9234 		if(byteOrder == ByteOrder.irrelevant && T.sizeof > 1)
9235 			throw new InvalidArgumentsException("byteOrder", "byte order must be specified for type " ~ T.stringof ~ " because it is bigger than one byte", "ReadableStream.get", file, line);
9236 
9237 		// FIXME: what if it is a struct?
9238 
9239 		while(bufferedLength() < T.sizeof)
9240 			waitForAdditionalData();
9241 
9242 		static if(T.sizeof == 1) {
9243 			ubyte ret = consumeOneByte();
9244 			return *cast(T*) &ret;
9245 		} else {
9246 			static if(T.sizeof == 8)
9247 				ulong ret;
9248 			else static if(T.sizeof == 4)
9249 				uint ret;
9250 			else static if(T.sizeof == 2)
9251 				ushort ret;
9252 			else static assert(0, "unimplemented type, try using just the basic types");
9253 
9254 			if(byteOrder == ByteOrder.littleEndian) {
9255 				typeof(ret) buffer;
9256 				foreach(b; 0 .. T.sizeof) {
9257 					buffer = consumeOneByte();
9258 					buffer <<= T.sizeof * 8 - 8;
9259 
9260 					ret >>= 8;
9261 					ret |= buffer;
9262 				}
9263 			} else {
9264 				foreach(b; 0 .. T.sizeof) {
9265 					ret <<= 8;
9266 					ret |= consumeOneByte();
9267 				}
9268 			}
9269 
9270 			return *cast(T*) &ret;
9271 		}
9272 	}
9273 
9274 	/// ditto
9275 	final T get(T : E[], E)(size_t length, ByteOrder elementByteOrder = ByteOrder.irrelevant, string file = __FILE__, size_t line = __LINE__) {
9276 		if(elementByteOrder == ByteOrder.irrelevant && E.sizeof > 1)
9277 			throw new InvalidArgumentsException("elementByteOrder", "byte order must be specified for type " ~ E.stringof ~ " because it is bigger than one byte", "ReadableStream.get", file, line);
9278 
9279 		// if the stream is closed before getting the length or the terminator, should we send partial stuff
9280 		// or just throw?
9281 
9282 		while(bufferedLength() < length * E.sizeof)
9283 			waitForAdditionalData();
9284 
9285 		T ret;
9286 
9287 		ret.length = length;
9288 
9289 		if(false && elementByteOrder == ByteOrder.irrelevant) {
9290 			// ret[] =
9291 			// FIXME: can prolly optimize
9292 		} else {
9293 			foreach(i; 0 .. length)
9294 				ret[i] = get!E(elementByteOrder);
9295 		}
9296 
9297 		return ret;
9298 
9299 	}
9300 
9301 	/// ditto
9302 	final T get(T : E[], E)(scope bool delegate(E e) isTerminatingSentinel, ByteOrder elementByteOrder = ByteOrder.irrelevant, string file = __FILE__, size_t line = __LINE__) {
9303 		if(elementByteOrder == ByteOrder.irrelevant && E.sizeof > 1)
9304 			throw new InvalidArgumentsException("elementByteOrder", "byte order must be specified for type " ~ E.stringof ~ " because it is bigger than one byte", "ReadableStream.get", file, line);
9305 
9306 		T ret;
9307 
9308 		do {
9309 			try
9310 				ret ~= get!E(elementByteOrder);
9311 			catch(ArsdException!"is already closed" ae)
9312 				return ret;
9313 		} while(!isTerminatingSentinel(ret[$-1]));
9314 
9315 		return ret[0 .. $-1]; // cut off the terminating sentinel
9316 	}
9317 
9318 	/++
9319 
9320 	+/
9321 	bool isClosed() {
9322 		return isClosed_ && currentBuffer.length == 0 && leftoverBuffer.length == 0;
9323 	}
9324 
9325 	// Control side of things
9326 
9327 	private bool isClosed_;
9328 
9329 	/++
9330 		Feeds data into the stream, which can be consumed by `get`. If a task is waiting for more
9331 		data to satisfy its get requests, this will trigger those tasks to resume.
9332 
9333 		If you feed it empty data, it will mark the stream as closed.
9334 	+/
9335 	void feedData(ubyte[] data) {
9336 		if(data.length == 0)
9337 			isClosed_ = true;
9338 
9339 		currentBuffer = data;
9340 		// this is a borrowed buffer, so we won't keep the reference long term
9341 		scope(exit)
9342 			currentBuffer = null;
9343 
9344 		if(waitingTask !is null) {
9345 			waitingTask.call();
9346 		}
9347 	}
9348 
9349 	/++
9350 		You basically have to use this thing from a task
9351 	+/
9352 	protected void waitForAdditionalData() {
9353 		if(isClosed_)
9354 			throw ArsdException!("is already closed")();
9355 
9356 		Fiber task = Fiber.getThis;
9357 
9358 		assert(task !is null);
9359 
9360 		if(waitingTask !is null && waitingTask !is task)
9361 			throw new ArsdException!"streams can only have one waiting task";
9362 
9363 		// copy any pending data in our buffer to the longer-term buffer
9364 		if(currentBuffer.length)
9365 			leftoverBuffer ~= currentBuffer;
9366 
9367 		waitingTask = task;
9368 		task.yield();
9369 	}
9370 
9371 	private Fiber waitingTask;
9372 	private ubyte[] leftoverBuffer;
9373 	private ubyte[] currentBuffer;
9374 
9375 	private size_t bufferedLength() {
9376 		return leftoverBuffer.length + currentBuffer.length;
9377 	}
9378 
9379 	private ubyte consumeOneByte() {
9380 		ubyte b;
9381 		if(leftoverBuffer.length) {
9382 			b = leftoverBuffer[0];
9383 			leftoverBuffer = leftoverBuffer[1 .. $];
9384 		} else if(currentBuffer.length) {
9385 			b = currentBuffer[0];
9386 			currentBuffer = currentBuffer[1 .. $];
9387 		} else {
9388 			assert(0, "consuming off an empty buffer is impossible");
9389 		}
9390 
9391 		return b;
9392 	}
9393 }
9394 
9395 // FIXME: do a stringstream too
9396 
9397 unittest {
9398 	auto stream = new ReadableStream();
9399 
9400 	int position;
9401 	char[16] errorBuffer;
9402 
9403 	auto fiber = new Fiber(() {
9404 		position = 1;
9405 		int a = stream.get!int(ByteOrder.littleEndian);
9406 		assert(a == 10, intToString(a, errorBuffer[]));
9407 		position = 2;
9408 		ubyte b = stream.get!ubyte;
9409 		assert(b == 33);
9410 		position = 3;
9411 
9412 		// ubyte[] c = stream.get!(ubyte[])(3);
9413 		// int[] d = stream.get!(int[])(3);
9414 	});
9415 
9416 	fiber.call();
9417 	assert(position == 1);
9418 	stream.feedData([10, 0, 0, 0]);
9419 	assert(position == 2);
9420 	stream.feedData([33]);
9421 	assert(position == 3);
9422 
9423 	// stream.feedData([1,2,3]);
9424 	// stream.feedData([1,2,3,4,1,2,3,4,1,2,3,4]);
9425 }
9426 
9427 private char[] asciiToUpper(scope const(char)[] s) pure {
9428 	char[] copy = s.dup;
9429 	foreach(ref ch; copy) {
9430 		if(ch >= 'a' && ch <= 'z')
9431 			ch -= 32;
9432 	}
9433 	return copy;
9434 }
9435 
9436 /++
9437 	UNSTABLE, NOT FULLY IMPLEMENTED. DO NOT USE YET.
9438 
9439 	You might use this like:
9440 
9441 	---
9442 	auto proc = new ExternalProcess();
9443 	auto stdoutStream = new ReadableStream();
9444 
9445 	// to use a stream you can make one and have a task consume it
9446 	runTask({
9447 		while(!stdoutStream.isClosed) {
9448 			auto line = stdoutStream.get!string(e => e == '\n');
9449 		}
9450 	});
9451 
9452 	// then make the process feed into the stream
9453 	proc.onStdoutAvailable = (got) {
9454 		stdoutStream.feedData(got); // send it to the stream for processing
9455 		stdout.rawWrite(got); // forward it through to our own thing
9456 		// could also append it to a buffer to return it on complete
9457 	};
9458 	proc.start();
9459 	---
9460 
9461 	Please note that this does not currently and I have no plans as of this writing to add support for any kind of direct file descriptor passing. It always pipes them back to the parent for processing. If you don't want this, call the lower level functions yourself; the reason this class is here is to aid integration in the arsd.core event loop. Of course, I might change my mind on this.
9462 +/
9463 class ExternalProcess /*: AsyncOperationRequest*/ {
9464 
9465 	private static version(Posix) {
9466 		__gshared ExternalProcess[pid_t] activeChildren;
9467 
9468 		void recordChildCreated(pid_t pid, ExternalProcess proc) {
9469 			synchronized(typeid(ExternalProcess)) {
9470 				activeChildren[pid] = proc;
9471 			}
9472 		}
9473 
9474 		void recordChildChanged(pid_t pid, int status) {
9475 			synchronized(typeid(ExternalProcess)) {
9476 				if(pid in activeChildren) {
9477 					auto ac = activeChildren[pid];
9478 
9479 					// import unix = core.sys.posix.unistd; unix.write(1, "SIGCHLD\n".ptr, 8);
9480 
9481 					import core.sys.posix.sys.wait;
9482 					if(WIFEXITED(status)) {
9483 						// exited normally
9484 						ac.markComplete(WEXITSTATUS(status));
9485 						activeChildren.remove(pid);
9486 					} else if(WIFSIGNALED(status)) {
9487 						// terminated by signal
9488 
9489 						// version(linux) import core.sys.linux.sys.wait : WCOREDUMP;
9490 
9491 						bool coredumped;
9492 						static if(is(typeof(WCOREDUMP))) {
9493 							if(WCOREDUMP(status)) {
9494 								coredumped = true;
9495 							}
9496 						}
9497 
9498 						ac.markTerminatedBySignal(WTERMSIG(status), coredumped);
9499 						activeChildren.remove(pid);
9500 					} else if(WIFSTOPPED(status)) {
9501 						// stopped by signal
9502 						ac.markStoppedBySignal(WSTOPSIG(status));
9503 					} else if(WIFCONTINUED(status)) {
9504 						// continued by SIGCONT
9505 						ac.markContinued();
9506 					} else {
9507 						// unknown condition......
9508 					}
9509 				}
9510 			}
9511 		}
9512 	}
9513 
9514 	// FIXME: config to pass through a shell or not
9515 
9516 	/++
9517 		This is the native version for Windows.
9518 	+/
9519 	version(Windows)
9520 	this(FilePath program, string commandLine) {
9521 		version(Posix) {
9522 			assert(0, "not implemented command line to posix args yet");
9523 		} else version(Windows) {
9524 			this.program = program;
9525 			this.commandLine = commandLine;
9526 		}
9527 		else throw new NotYetImplementedException();
9528 	}
9529 
9530 	/+
9531 	this(string commandLine) {
9532 		version(Posix) {
9533 			assert(0, "not implemented command line to posix args yet");
9534 		}
9535 		else throw new NotYetImplementedException();
9536 	}
9537 
9538 	this(string[] args) {
9539 		version(Posix) {
9540 			this.program = FilePath(args[0]);
9541 			this.args = args;
9542 		}
9543 		else throw new NotYetImplementedException();
9544 	}
9545 	+/
9546 
9547 	/++
9548 		This is the native version for Posix.
9549 	+/
9550 	version(Posix)
9551 	this(FilePath program, string[] args) {
9552 		version(Posix) {
9553 			this.program = program;
9554 			this.args = args;
9555 		}
9556 		else throw new NotYetImplementedException();
9557 	}
9558 
9559 	/++
9560 		This allows you to record a process as existing to the core event loop,
9561 		so you get completion and other notifications on it, but without doing any
9562 		other processing. The process should already exist as a child of our main process
9563 		and you should not attempt to use any of the i/o files on it, as they will be null.
9564 
9565 		History:
9566 			Added December 18, 2025
9567 	+/
9568 	version(Posix)
9569 	this(pid_t recordForMinimalWrapping) {
9570 		recordChildCreated(recordForMinimalWrapping, this);
9571 	}
9572 
9573 	version(Posix)
9574 	package {
9575 		// if you use the override thing, it is YOUR responsibility to close them!
9576 		int overrideStdin = -2;
9577 		int overrideStdout = -2;
9578 		int overrideStderr = -2;
9579 		int pgid = -2;
9580 
9581 		const(char*)* environment;
9582 
9583 		// FIXME: change it to string[string]
9584 		// it will modify the passed AA
9585 		void setEnvironmentWithModifications(string[string] mods) @system {
9586 			const(char*)[] ret;
9587 
9588 			const(char*)* head = environ;
9589 			while(*head) {
9590 				auto headz = stringz(*head);
9591 				// see if head and any of the mods are the same var
9592 				auto headd = headz.borrow;
9593 				auto equal = headd.indexOf("=");
9594 				if(equal == -1)
9595 					equal = cast(int) headd.length;
9596 				auto name = headd[0 .. equal];
9597 				if(name in mods) {
9598 					ret ~= cast(char*) (name ~ "=" ~ mods[name] ~ "\0").ptr;
9599 					mods.remove(cast(string) name);
9600 				} else {
9601 					ret ~= *head;
9602 				}
9603 
9604 				head++;
9605 			}
9606 
9607 			// append the remainder of mods to the ret
9608 			foreach(name, value; mods)
9609 				ret ~= cast(char*) (name ~ "=" ~ value ~ "\0").ptr;
9610 
9611 			ret ~= null;
9612 
9613 			environment = ret.ptr;
9614 		}
9615 	}
9616 
9617 	version(Windows)
9618 	package {
9619 		// if you use the override thing, it is YOUR responsibility to close them!
9620 		HANDLE overrideStdin = INVALID_HANDLE_VALUE;
9621 		HANDLE overrideStdout = INVALID_HANDLE_VALUE;
9622 		HANDLE overrideStderr = INVALID_HANDLE_VALUE;
9623 
9624 		wchar* environment;
9625 
9626 		void setEnvironmentWithModifications(string[string] mods) @system {
9627 			wchar[] ret;
9628 
9629 			// FIXME: case sensitivity in name lookup,the passed mods should all be uppercase
9630 
9631 			// FIXME: "All strings in the environment block must be sorted alphabetically by name. The sort is case-insensitive, Unicode order, without regard to locale."
9632 
9633 			auto originalEnv = GetEnvironmentStringsW();
9634 			if(originalEnv is null)
9635 				throw new WindowsApiException("GetEnvironmentStringsW",GetLastError());
9636 			scope(exit) {
9637 				if(originalEnv)
9638 					FreeEnvironmentStringsW(originalEnv);
9639 			}
9640 
9641 			// read null terminated strings until we hit one of zero length
9642 			// create a new block of memory with the same data, but all copied
9643 			auto env = originalEnv;
9644 			more:
9645 			wchar* start = env;
9646 			while(*env) {
9647 				env++;
9648 			}
9649 			wchar[] wv = start[0 .. env - start];
9650 			if(wv.length) {
9651 				string v = makeUtf8StringFromWindowsString(wv);
9652 				auto equal = v.indexOf("=");
9653 				if(equal == -1)
9654 					equal = cast(int) v.length;
9655 				auto name = v[0 .. equal].asciiToUpper;
9656 
9657 				if(name in mods) {
9658 					WCharzBuffer bfr = (name ~ "=" ~ mods[name]);
9659 					ret ~= bfr.ptr[0 .. bfr.length + 1]; // to include the zero terminator
9660 					mods.remove(cast(string) name);
9661 				} else {
9662 					ret ~= start[0 .. env - start + 1]; // include zero terminator
9663 				}
9664 
9665 				env++; // move past the zero terminator
9666 				goto more;
9667 			}
9668 
9669 			foreach(name, mod; mods) {
9670 				WCharzBuffer bfr = (name ~ "=" ~ mod);
9671 				ret ~= bfr.ptr[0 .. bfr.length + 1]; // to include the zero terminator
9672 			}
9673 
9674 			ret ~= 0;
9675 
9676 			this.environment = ret.ptr;
9677 		}
9678 	}
9679 
9680 	/++
9681 
9682 	+/
9683 	void start() {
9684 		version(Posix) {
9685 			getThisThreadEventLoop(); // ensure it is initialized
9686 
9687 			int ret;
9688 
9689 			int[2] stdinPipes;
9690 			if(overrideStdin == -2) {
9691 				ret = pipe(stdinPipes);
9692 				if(ret == -1)
9693 					throw new ErrnoApiException("stdin pipe", errno);
9694 			}
9695 
9696 			scope(failure) {
9697 				if(overrideStdin == -2) {
9698 					close(stdinPipes[0]);
9699 					close(stdinPipes[1]);
9700 				}
9701 			}
9702 
9703 			auto stdinFd = overrideStdin == -2 ? stdinPipes[1] : -1;
9704 
9705 			int[2] stdoutPipes;
9706 			if(overrideStdout == -2) {
9707 				ret = pipe(stdoutPipes);
9708 				if(ret == -1)
9709 					throw new ErrnoApiException("stdout pipe", errno);
9710 			}
9711 
9712 			scope(failure) {
9713 				if(overrideStdout == -2) {
9714 					close(stdoutPipes[0]);
9715 					close(stdoutPipes[1]);
9716 				}
9717 			}
9718 
9719 			auto stdoutFd = overrideStdout == -2 ? stdoutPipes[0] : -1;
9720 
9721 			int[2] stderrPipes;
9722 			if(overrideStderr == -2) {
9723 				ret = pipe(stderrPipes);
9724 				if(ret == -1)
9725 					throw new ErrnoApiException("stderr pipe", errno);
9726 			}
9727 
9728 			scope(failure) {
9729 				if(overrideStderr == -2) {
9730 					close(stderrPipes[0]);
9731 					close(stderrPipes[1]);
9732 				}
9733 			}
9734 
9735 			auto stderrFd = overrideStderr == -2 ? stderrPipes[0] : -1;
9736 
9737 
9738 			int[2] errorReportPipes;
9739 			ret = pipe(errorReportPipes);
9740 			if(ret == -1)
9741 				throw new ErrnoApiException("error reporting pipe", errno);
9742 
9743 			scope(failure) {
9744 				close(errorReportPipes[0]);
9745 				close(errorReportPipes[1]);
9746 			}
9747 
9748 			setCloExec(errorReportPipes[0]);
9749 			setCloExec(errorReportPipes[1]);
9750 
9751 			// writeln(pgid);
9752 			auto forkRet = fork();
9753 			if(forkRet == -1)
9754 				throw new ErrnoApiException("fork", errno);
9755 			if(forkRet == 0) {
9756 				// child side
9757 
9758 				// FIXME can we do more error checking that is actually useful here?
9759 				// these operations are virtually guaranteed to succeed given the setup anyway.
9760 
9761 				// FIXME pty too
9762 
9763 				void fail(int step) {
9764 					import core.stdc.errno;
9765 					auto code = errno;
9766 
9767 					// report the info back to the parent then exit
9768 
9769 					int[2] msg = [step, code];
9770 					auto ret = write(errorReportPipes[1], msg.ptr, msg.sizeof);
9771 
9772 					// but if this fails there's not much we can do...
9773 
9774 					import core.stdc.stdlib;
9775 					exit(1);
9776 				}
9777 
9778 				// both parent and child are supposed to try to set it
9779 				if(pgid != -2) {
9780 					setpgid(0, pgid == 0 ? getpid() : pgid);
9781 				}
9782 
9783 				// dup2 closes the fd it is replacing automatically
9784 				// then don't need either of the original pipe fds anymore
9785 				if(overrideStdin == -2) {
9786 					dup2(stdinPipes[0], 0);
9787 					close(stdinPipes[0]);
9788 					close(stdinPipes[1]);
9789 				} else if(overrideStdin != 0) {
9790 					dup2(overrideStdin, 0);
9791 					close(overrideStdin);
9792 				}
9793 
9794 				if(overrideStdout == -2) {
9795 					dup2(stdoutPipes[1], 1);
9796 					close(stdoutPipes[0]);
9797 					close(stdoutPipes[1]);
9798 				} else if(overrideStdout != 1) {
9799 					dup2(overrideStdout, 1);
9800 					close(overrideStdout);
9801 				}
9802 
9803 				if(overrideStderr == -2) {
9804 					dup2(stderrPipes[1], 2);
9805 					close(stderrPipes[0]);
9806 					close(stderrPipes[1]);
9807 				} else if(overrideStderr != 2) {
9808 					dup2(overrideStderr, 2);
9809 					close(overrideStderr);
9810 				}
9811 
9812 				// the error reporting pipe will be closed upon exec since we set cloexec before fork
9813 				// and everything else should have cloexec set too hopefully.
9814 
9815 				if(beforeExec)
9816 					beforeExec();
9817 
9818 				// i'm not sure that a fully-initialized druntime is still usable
9819 				// after a fork(), so i'm gonna stick to the C lib in here.
9820 
9821 				const(char)* file = mallocedStringz(program.path).ptr;
9822 				if(file is null)
9823 					fail(1);
9824 				const(char)*[] argv = mallocSlice!(const(char)*)(args.length + 1);
9825 				if(argv is null)
9826 					fail(2);
9827 				foreach(idx, arg; args) {
9828 					argv[idx] = mallocedStringz(args[idx]).ptr;
9829 					if(argv[idx] is null)
9830 						fail(3);
9831 				}
9832 				argv[args.length] = null;
9833 
9834 				auto rete = execve(file, argv.ptr, this.environment is null ? environ : this.environment); // FIXME: i used to use execvp, which searches path but i think i like this more
9835 				if(rete == -1) {
9836 					fail(4);
9837 				} else {
9838 					// unreachable code, exec never returns if it succeeds
9839 					assert(0);
9840 				}
9841 			} else {
9842 				pid = forkRet;
9843 
9844 				// both parent and child are supposed to try to set it
9845 				if(pgid != -2) {
9846 					setpgid(pid, pgid == 0 ? pid : pgid);
9847 				}
9848 
9849 				recordChildCreated(pid, this);
9850 
9851 				// close our copy of the write side of the error reporting pipe
9852 				// so the read will immediately give eof when the fork closes it too
9853 				ErrnoEnforce!close(errorReportPipes[1]);
9854 
9855 				int[2] msg;
9856 				// this will block to wait for it to actually either start up or fail to exec (which should be near instant)
9857 				try_again:
9858 				auto val = read(errorReportPipes[0], msg.ptr, msg.sizeof);
9859 
9860 				if(val == -1) {
9861 					if(errno == EINTR)
9862 						goto try_again;
9863 					throw new ErrnoApiException("read error report", errno);
9864 				}
9865 
9866 				if(val == msg.sizeof) {
9867 					// error happened
9868 					// FIXME: keep the step part of the error report too
9869 					throw new ErrnoApiException("exec", msg[1]);
9870 				} else if(val == 0) {
9871 					// pipe closed, meaning exec succeeded
9872 				} else {
9873 					assert(0); // never supposed to happen
9874 				}
9875 
9876 				// set the ones we keep to close upon future execs
9877 				// and close the others
9878 				if(overrideStdin == -2) {
9879 					setCloExec(stdinPipes[1]);
9880 					ErrnoEnforce!close(stdinPipes[0]);
9881 					makeNonBlocking(stdinFd);
9882 					_stdin = new AsyncFile(stdinFd);
9883 				}
9884 
9885 				if(overrideStdout == -2) {
9886 					setCloExec(stdoutPipes[0]);
9887 					ErrnoEnforce!close(stdoutPipes[1]);
9888 					makeNonBlocking(stdoutFd);
9889 					_stdout = new AsyncFile(stdoutFd);
9890 				}
9891 
9892 				if(overrideStderr == -2) {
9893 					setCloExec(stderrPipes[0]);
9894 					ErrnoEnforce!close(stderrPipes[1]);
9895 					makeNonBlocking(stderrFd);
9896 					_stderr = new AsyncFile(stderrFd);
9897 				}
9898 
9899 				ErrnoEnforce!close(errorReportPipes[0]);
9900 			}
9901 		} else version(Windows) {
9902 			WCharzBuffer program = this.program.path;
9903 			WCharzBuffer cmdLine = this.commandLine;
9904 
9905 			PROCESS_INFORMATION pi;
9906 			STARTUPINFOW startupInfo;
9907 
9908 			SECURITY_ATTRIBUTES saAttr;
9909 			saAttr.nLength = SECURITY_ATTRIBUTES.sizeof;
9910 			saAttr.bInheritHandle = true;
9911 			saAttr.lpSecurityDescriptor = null;
9912 
9913 			HANDLE inreadPipe;
9914 			HANDLE inwritePipe;
9915 
9916 			if(overrideStdin == INVALID_HANDLE_VALUE) {
9917 				if(MyCreatePipeEx(&inreadPipe, &inwritePipe, &saAttr, 0, 0, FILE_FLAG_OVERLAPPED) == 0)
9918 					throw new WindowsApiException("CreatePipe", GetLastError());
9919 				if(!SetHandleInformation(inwritePipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
9920 					throw new WindowsApiException("SetHandleInformation", GetLastError());
9921 			}
9922 
9923 			scope(failure) {
9924 				if(overrideStdin == INVALID_HANDLE_VALUE) {
9925 					CloseHandle(inreadPipe);
9926 					CloseHandle(inwritePipe);
9927 				}
9928 			}
9929 
9930 			HANDLE outreadPipe;
9931 			HANDLE outwritePipe;
9932 			if(overrideStdout == INVALID_HANDLE_VALUE) {
9933 				if(MyCreatePipeEx(&outreadPipe, &outwritePipe, &saAttr, 0, FILE_FLAG_OVERLAPPED, 0) == 0)
9934 					throw new WindowsApiException("CreatePipe", GetLastError());
9935 				if(!SetHandleInformation(outreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
9936 					throw new WindowsApiException("SetHandleInformation", GetLastError());
9937 			}
9938 
9939 			scope(failure) {
9940 				if(overrideStdout == INVALID_HANDLE_VALUE) {
9941 					CloseHandle(outreadPipe);
9942 					CloseHandle(outwritePipe);
9943 				}
9944 			}
9945 
9946 
9947 			HANDLE errreadPipe;
9948 			HANDLE errwritePipe;
9949 			if(overrideStderr == INVALID_HANDLE_VALUE) {
9950 				if(MyCreatePipeEx(&errreadPipe, &errwritePipe, &saAttr, 0, FILE_FLAG_OVERLAPPED, 0) == 0)
9951 					throw new WindowsApiException("CreatePipe", GetLastError());
9952 				if(!SetHandleInformation(errreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
9953 					throw new WindowsApiException("SetHandleInformation", GetLastError());
9954 			}
9955 
9956 			scope(failure) {
9957 				if(overrideStderr == INVALID_HANDLE_VALUE) {
9958 					CloseHandle(errreadPipe);
9959 					CloseHandle(errwritePipe);
9960 				}
9961 			}
9962 
9963 
9964 			startupInfo.cb = startupInfo.sizeof;
9965 			startupInfo.dwFlags = STARTF_USESTDHANDLES;
9966 			startupInfo.hStdInput = (overrideStdin == INVALID_HANDLE_VALUE) ? inreadPipe : overrideStdin;
9967 			startupInfo.hStdOutput = (overrideStdout == INVALID_HANDLE_VALUE) ? outwritePipe : overrideStdout;
9968 			startupInfo.hStdError = (overrideStderr == INVALID_HANDLE_VALUE) ? errwritePipe : overrideStderr;
9969 
9970 			auto result = CreateProcessW(
9971 				program.ptr,
9972 				cmdLine.ptr,
9973 				null, // process attributes
9974 				null, // thread attributes
9975 				true, // inherit handles; necessary for the std in/out/err ones to work
9976 				this.environment is null ? 0 : CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags FIXME might be useful to change
9977 				this.environment, // environment, might be worth changing
9978 				null, // current directory
9979 				&startupInfo,
9980 				&pi
9981 			);
9982 
9983 			if(!result)
9984 				throw new WindowsApiException("CreateProcessW", GetLastError());
9985 
9986 			if(overrideStdin == INVALID_HANDLE_VALUE) {
9987 				_stdin = new AsyncFile(inwritePipe);
9988 				Win32Enforce!CloseHandle(inreadPipe);
9989 			}
9990 			if(overrideStdout == INVALID_HANDLE_VALUE) {
9991 				_stdout = new AsyncFile(outreadPipe);
9992 				Win32Enforce!CloseHandle(outwritePipe);
9993 			}
9994 			if(overrideStderr == INVALID_HANDLE_VALUE) {
9995 				_stderr = new AsyncFile(errreadPipe);
9996 				Win32Enforce!CloseHandle(errwritePipe);
9997 			}
9998 
9999 			Win32Enforce!CloseHandle(pi.hThread);
10000 
10001 			handle = pi.hProcess;
10002 
10003 			procRegistration = getThisThreadEventLoop.addCallbackOnHandleReady(handle, new CallbackHelper(&almostComplete));
10004 		}
10005 	}
10006 
10007 	version(Windows) {
10008 		private HANDLE handle;
10009 		private FilePath program;
10010 		private string commandLine;
10011 		private ICoreEventLoop.UnregisterToken procRegistration;
10012 
10013 		private final void almostComplete() {
10014 			// GetProcessTimes lol
10015 			Win32Enforce!GetExitCodeProcess(handle, cast(uint*) &_status);
10016 
10017 			markComplete(_status);
10018 
10019 			procRegistration.unregister();
10020 			CloseHandle(handle);
10021 			this.completed = true;
10022 		}
10023 	} else version(Posix) {
10024 		import core.sys.posix.unistd;
10025 		import core.sys.posix.fcntl;
10026 
10027 		package pid_t pid = -1;
10028 
10029 		public void delegate() beforeExec;
10030 
10031 		private FilePath program;
10032 		private string[] args;
10033 	}
10034 
10035 	private final void markComplete(int status) {
10036 		completed = true;
10037 		_status = status;
10038 
10039 		if(oncomplete)
10040 			oncomplete(this);
10041 	}
10042 	private final void markTerminatedBySignal(int signal, bool coredumped) {
10043 		completed = true;
10044 		_status = -signal;
10045 		this.coredumped = coredumped;
10046 
10047 		if(oncomplete)
10048 			oncomplete(this);
10049 	}
10050 	private final void markStoppedBySignal(int signal) {
10051 		stopped = true;
10052 		_status = -signal;
10053 	}
10054 	private final void markContinued() {
10055 		stopped = false;
10056 		_status = int.min;
10057 	}
10058 
10059 	private AsyncFile _stdin;
10060 	private AsyncFile _stdout;
10061 	private AsyncFile _stderr;
10062 
10063 	/++
10064 
10065 	+/
10066 	AsyncFile stdin() {
10067 		return _stdin;
10068 	}
10069 	/// ditto
10070 	AsyncFile stdout() {
10071 		return _stdout;
10072 	}
10073 	/// ditto
10074 	AsyncFile stderr() {
10075 		return _stderr;
10076 	}
10077 
10078 	/++
10079 	+/
10080 	void waitForCompletion() {
10081 		getThisThreadEventLoop().run(&this.isComplete);
10082 	}
10083 
10084 	/++
10085 		History:
10086 			Added November 23, 2025
10087 	+/
10088 	void waitForChange() {
10089 		bool stoppedAtFirst = isStopped;
10090 		getThisThreadEventLoop().run(() { return this.isComplete || (stoppedAtFirst != this.isStopped); });
10091 	}
10092 
10093 	/++
10094 	+/
10095 	bool isComplete() {
10096 		return completed;
10097 	}
10098 	/++
10099 		History:
10100 			Added November 23, 2025
10101 	+/
10102 	bool isStopped() {
10103 		return stopped;
10104 	}
10105 	/++
10106 		History:
10107 			Added November 23, 2025
10108 	+/
10109 	bool leftCoreDump() {
10110 		return coredumped;
10111 	}
10112 
10113 	private bool coredumped;
10114 	private bool stopped;
10115 	private bool completed;
10116 	private int _status = int.min;
10117 
10118 	/++
10119 	+/
10120 	int status() {
10121 		return _status;
10122 	}
10123 
10124 	// void delegate(int code) onTermination;
10125 
10126 	void delegate(ExternalProcess) oncomplete;
10127 
10128 	// pty?
10129 }
10130 
10131 // FIXME: comment this out
10132 /+
10133 unittest {
10134 	auto proc = new ExternalProcess(FilePath("/bin/cat"), ["/bin/cat"]);
10135 
10136 	getThisThreadEventLoop(); // initialize it
10137 
10138 	int c = 0;
10139 	proc.onStdoutAvailable = delegate(ubyte[] got) {
10140 		if(c == 0)
10141 			assert(cast(string) got == "hello!");
10142 		else
10143 			assert(got.length == 0);
10144 			// import std.stdio; writeln(got);
10145 		c++;
10146 	};
10147 
10148 	proc.start();
10149 
10150 	assert(proc.pid != -1);
10151 
10152 
10153 	import std.stdio;
10154 	Thread[4] pool;
10155 
10156 	bool shouldExit;
10157 
10158 	static int received;
10159 
10160 	proc.writeToStdin("hello!");
10161 	proc.writeToStdin(null); // closes the pipe
10162 
10163 	proc.waitForCompletion();
10164 
10165 	assert(proc.status == 0);
10166 
10167 	assert(c == 2);
10168 
10169 	// writeln("here");
10170 }
10171 +/
10172 
10173 // to test the thundering herd on signal handling
10174 version(none)
10175 unittest {
10176 	Thread[4] pool;
10177 	foreach(ref thread; pool) {
10178 		thread = new class Thread {
10179 			this() {
10180 				super({
10181 					int count;
10182 					getThisThreadEventLoop().run(() {
10183 						if(count > 4) return true;
10184 						count++;
10185 						return false;
10186 					});
10187 				});
10188 			}
10189 		};
10190 		thread.start();
10191 	}
10192 	foreach(ref thread; pool) {
10193 		thread.join();
10194 	}
10195 }
10196 
10197 /+
10198 	================
10199 	LOGGER FRAMEWORK
10200 	================
10201 +/
10202 /++
10203 	DO NOT USE THIS YET IT IS NOT FUNCTIONAL NOR STABLE
10204 
10205 
10206 	The arsd.core logger works differently than many in that it works as a ring buffer of objects that are consumed (or missed; buffer overruns are possible) by a different thread instead of strings written to some file.
10207 
10208 	A library (or an application) defines a log source. They write to this source.
10209 
10210 	Applications then define log listeners, zero or more, which reads from various sources and does something with them.
10211 
10212 	Log calls, in this sense, are quite similar to asynchronous events that can be subscribed to by event handlers. The difference is events are generally not dropped - they might coalesce but are usually not just plain dropped in a buffer overrun - whereas logs can be. If the log consumer can't keep up, the details are just lost. The log producer will not wait for the consumer to catch up.
10213 
10214 
10215 	An application can also set a default subscriber which applies to all log objects throughout.
10216 
10217 	All log message objects must be capable of being converted to strings and to json.
10218 
10219 	Ad-hoc messages can be done with interpolated sequences.
10220 
10221 	Messages automatically get a timestamp. They can also have file/line and maybe even a call stack.
10222 
10223 	Examples:
10224 	---
10225 		auto logger = new shared LoggerOf!GenericEmbeddableInterpolatedSequence;
10226 
10227 		mylogger.info(i"$this heartbeat");
10228 	---
10229 
10230 	History:
10231 		Added May 27, 2024
10232 
10233 		Not actually implemented until February 6, 2025, when it changed from mixin template to class.
10234 +/
10235 class LoggerOf(T, size_t bufferSize = 16) : SynchronizableObject {
10236 	private LoggedMessage!T[bufferSize] ring;
10237 	private ulong writeBufferPosition;
10238 
10239 	import core.sync.mutex;
10240 	import core.sync.condition;
10241 
10242 	private Mutex mutex;
10243 	private Condition condition;
10244 	private bool active;
10245 	private int listenerCount;
10246 
10247 	this() shared {
10248 		mutex = new shared Mutex(cast(LoggerOf) this);
10249 		condition = new shared Condition(mutex);
10250 		active = true;
10251 	}
10252 
10253 	/++
10254 		Closes the log channel and waits for all listeners to finish pending work before returning.
10255 
10256 		Once the logger is closed, it cannot be used again.
10257 
10258 		You should close any logger you attached listeners to before returning from `main()`.
10259 	+/
10260 	void close() shared {
10261 		synchronized(this) {
10262 			active = false;
10263 			condition.notifyAll();
10264 
10265 			while(listenerCount > 0) {
10266 				condition.wait();
10267 			}
10268 		}
10269 	}
10270 
10271 	/++
10272 
10273 		Examples:
10274 
10275 		---
10276 		// to write all messages to the console
10277 		logger.addListener((message, missedMessageCount) {
10278 			writeln(message);
10279 		});
10280 		---
10281 
10282 		---
10283 		// to only write warnings and errors
10284 		logger.addListener((message, missedMessageCount) {
10285 			if(message.level >= LogLevel.warn)
10286 				writeln(message);
10287 		});
10288 		---
10289 
10290 		---
10291 		// to ignore messages from arsd.core
10292 		logger.addListener((message, missedMessageCount) {
10293 			if(message.sourceLocation.moduleName != "arsd.core")
10294 				writeln(message);
10295 		});
10296 		---
10297 	+/
10298 	LogListenerController addListener(void delegate(LoggedMessage!T message, int missedMessages) dg) shared {
10299 		static class Listener : Thread, LogListenerController {
10300 			shared LoggerOf logger;
10301 			ulong readBufferPosition;
10302 			void delegate(LoggedMessage!T, int) dg;
10303 
10304 			bool connected;
10305 
10306 			import core.sync.event;
10307 			Event event;
10308 
10309 			this(shared LoggerOf logger, void delegate(LoggedMessage!T msg, int) dg) {
10310 				this.dg = dg;
10311 				this.logger = logger;
10312 				this.connected = true;
10313 				this.isDaemon = true;
10314 
10315 				auto us = cast(LoggerOf) logger;
10316 				synchronized(logger)
10317 					us.listenerCount++;
10318 
10319 				event.initialize(true, false);
10320 				super(&run);
10321 			}
10322 
10323 			void disconnect() {
10324 				this.connected = false;
10325 			}
10326 
10327 			void run() {
10328 				auto us = cast(LoggerOf) logger;
10329 				/+
10330 				// can't do this due to https://github.com/ldc-developers/ldc/issues/4837
10331 				// so doing the try/catch below and putting this under it
10332 				scope(exit) {
10333 					synchronized(logger) {
10334 						us.listenerCount--;
10335 						logger.condition.notifyAll();
10336 					}
10337 					// mark us as complete for other listeners waiting as well
10338 					static if (__traits(hasMember, event, "setIfInitialized")) {
10339 						// Upstream compatibility, see <https://github.com/dlang/dmd/pull/15800>.
10340 						event.setIfInitialized();
10341 					} else {
10342 						// Old D runtime compatibility
10343 						event.set();
10344 					}
10345 				}
10346 				+/
10347 
10348 				try {
10349 
10350 					LoggedMessage!T[bufferSize] buffer;
10351 					do {
10352 						int missedMessages = 0;
10353 						long n;
10354 						synchronized(logger) {
10355 							while(logger.active && connected && logger.writeBufferPosition <= readBufferPosition) {
10356 								logger.condition.wait();
10357 							}
10358 
10359 							n = us.writeBufferPosition - readBufferPosition;
10360 							if(n > bufferSize) {
10361 								// we missed something...
10362 								missedMessages = cast(int) (n - bufferSize);
10363 								readBufferPosition = us.writeBufferPosition - bufferSize;
10364 								n = bufferSize;
10365 							}
10366 							auto startPos = readBufferPosition % bufferSize;
10367 							auto endPos = us.writeBufferPosition % bufferSize;
10368 							if(endPos > startPos) {
10369 								buffer[0 .. cast(size_t) n] = us.ring[cast(size_t) startPos .. cast(size_t) endPos];
10370 							} else {
10371 								auto ourSplit = us.ring.length - startPos;
10372 								buffer[0 .. cast(size_t) ourSplit] = us.ring[cast(size_t) startPos .. $];
10373 								buffer[cast(size_t) ourSplit .. cast(size_t) (ourSplit + endPos)] = us.ring[0 .. cast(size_t) endPos];
10374 							}
10375 							readBufferPosition = us.writeBufferPosition;
10376 						}
10377 						foreach(item; buffer[0 .. cast(size_t) n]) {
10378 							if(!connected)
10379 								break;
10380 							dg(item, missedMessages);
10381 							missedMessages = 0;
10382 						}
10383 					} while(logger.active && connected);
10384 
10385 				} catch(Throwable t) {
10386 					// i guess i could try to log the exception for other listeners to pick up...
10387 
10388 				}
10389 
10390 				synchronized(logger) {
10391 					us.listenerCount--;
10392 					logger.condition.notifyAll();
10393 				}
10394 				// mark us as complete for other listeners waiting as well
10395 				static if (__traits(hasMember, event, "setIfInitialized")) {
10396 					// Upstream compatibility, see <https://github.com/dlang/dmd/pull/15800>.
10397 					event.setIfInitialized();
10398 				} else {
10399 					// Old D runtime compatibility
10400 					event.set();
10401 				}
10402 
10403 			}
10404 
10405 			void waitForCompletion() {
10406 				event.wait();
10407 			}
10408 		}
10409 
10410 		auto listener = new Listener(this, dg);
10411 		listener.start();
10412 
10413 		return listener;
10414 	}
10415 
10416 	void log(LoggedMessage!T message) shared {
10417 		synchronized(this) {
10418 			auto unshared = cast() this;
10419 			unshared.ring[writeBufferPosition % bufferSize] = message;
10420 			unshared.writeBufferPosition += 1;
10421 
10422 			// import std.stdio; std.stdio.writeln(message);
10423 			condition.notifyAll();
10424 		}
10425 	}
10426 
10427 	/// ditto
10428 	void log(LogLevel level, T message, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
10429 		import core.stdc.time;
10430 		log(LoggedMessage!T(level, sourceLocation, SimplifiedUtcTimestamp.fromUnixTime(time(null)), Thread.getThis(), message));
10431 	}
10432 
10433 	/// ditto
10434 	void info(T message, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
10435 		log(LogLevel.info, message, sourceLocation);
10436 	}
10437 	/// ditto
10438 	void trace(T message, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
10439 		log(LogLevel.trace, message, sourceLocation);
10440 	}
10441 	/// ditto
10442 	void warn(T message, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
10443 		log(LogLevel.warn, message, sourceLocation);
10444 	}
10445 	/// ditto
10446 	void error(T message, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
10447 		log(LogLevel.error, message, sourceLocation);
10448 	}
10449 
10450 	static if(is(T == GenericEmbeddableInterpolatedSequence)) {
10451 		pragma(inline, true)
10452 		final void info(T...)(InterpolationHeader header, T message, InterpolationFooter footer, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
10453 			log(LogLevel.info, GenericEmbeddableInterpolatedSequence(header, message, footer), sourceLocation);
10454 		}
10455 		pragma(inline, true)
10456 		final void trace(T...)(InterpolationHeader header, T message, InterpolationFooter footer, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
10457 			log(LogLevel.trace, GenericEmbeddableInterpolatedSequence(header, message, footer), sourceLocation);
10458 		}
10459 		pragma(inline, true)
10460 		final void warn(T...)(InterpolationHeader header, T message, InterpolationFooter footer, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
10461 			log(LogLevel.warn, GenericEmbeddableInterpolatedSequence(header, message, footer), sourceLocation);
10462 		}
10463 		pragma(inline, true)
10464 		final void error(T...)(InterpolationHeader header, T message, InterpolationFooter footer, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
10465 			log(LogLevel.error, GenericEmbeddableInterpolatedSequence(header, message, footer), sourceLocation);
10466 		}
10467 	}
10468 }
10469 
10470 /// ditto
10471 interface LogListenerController {
10472 	/++
10473 		Disconnects from the log producer as soon as possible, possibly leaving messages
10474 		behind in the log buffer. Once disconnected, the log listener will terminate
10475 		asynchronously and cannot be reused. Use [waitForCompletion] to block your thread
10476 		until the termination is complete.
10477 	+/
10478 	void disconnect();
10479 
10480 	/++
10481 		Waits for the listener to finish its pending work and terminate. You should call
10482 		[disconnect] first to make it start to exit.
10483 	+/
10484 	void waitForCompletion();
10485 }
10486 
10487 /// ditto
10488 struct SourceLocation {
10489 	string moduleName;
10490 	size_t line;
10491 }
10492 
10493 /// ditto
10494 struct LoggedMessage(T) {
10495 	LogLevel level;
10496 	SourceLocation sourceLocation;
10497 	SimplifiedUtcTimestamp timestamp;
10498 	Thread originatingThread;
10499 	T message;
10500 
10501 	// process id can be assumed by the listener,
10502 	// since it is always the same; logs are sent and received by the same process.
10503 
10504 	string toString() {
10505 		string ret;
10506 
10507 		ret ~= sourceLocation.moduleName;
10508 		ret ~= ":";
10509 		ret ~= toStringInternal(sourceLocation.line);
10510 		ret ~= " ";
10511 		if(originatingThread) {
10512 			char[16] buffer;
10513 			ret ~= originatingThread.name.length ? originatingThread.name : intToString(cast(long) originatingThread.id, buffer, IntToStringArgs().withRadix(16));
10514 		}
10515 		ret ~= "[";
10516 		ret ~= toStringInternal(level);
10517 		ret ~= "] ";
10518 		ret ~= timestamp.toString();
10519 		ret ~= " ";
10520 		ret ~= message.toString();
10521 
10522 		return ret;
10523 	}
10524 	// callstack?
10525 }
10526 
10527 /// ditto
10528 enum LogLevel {
10529 	trace,
10530 	info,
10531 	warn,
10532 	error,
10533 }
10534 
10535 private shared(LoggerOf!GenericEmbeddableInterpolatedSequence) _commonLogger;
10536 shared(LoggerOf!GenericEmbeddableInterpolatedSequence) logger() {
10537 	if(_commonLogger is null) {
10538 		synchronized {
10539 			if(_commonLogger is null)
10540 				_commonLogger = new shared LoggerOf!GenericEmbeddableInterpolatedSequence;
10541 		}
10542 	}
10543 
10544 	return _commonLogger;
10545 }
10546 
10547 /++
10548 	Makes note of an exception you catch and otherwise ignore.
10549 
10550 	History:
10551 		Added April 17, 2025
10552 +/
10553 void logSwallowedException(Exception e) {
10554 	logger.error(InterpolationHeader(), e.toString(), InterpolationFooter());
10555 }
10556 
10557 /+
10558 // using this requires a newish compiler so we just uncomment when necessary
10559 unittest {
10560 	void main() {
10561 		auto logger = logger;// new shared LoggerOf!GenericEmbeddableInterpolatedSequence;
10562 		LogListenerController l1;
10563 		l1 = logger.addListener((msg, missedCount) {
10564 			if(missedCount)
10565 				writeln("T1: missed ", missedCount);
10566 			writeln("T1:" ~msg.toString());
10567 			//Thread.sleep(500.msecs);
10568 			//l1.disconnect();
10569 				Thread.sleep(1.msecs);
10570 		});
10571 		foreach(n; 0 .. 200) {
10572 			logger.info(i"hello world $n");
10573 			if(n % 6 == 0)
10574 				Thread.sleep(1.msecs);
10575 		}
10576 
10577 		logger.addListener((msg, missedCount) {
10578 			if(missedCount) writeln("T2 missed ", missedCount);
10579 			writeln("T2:" ~msg.toString());
10580 		});
10581 
10582 		Thread.sleep(500.msecs);
10583 		l1.disconnect;
10584 		l1.waitForCompletion;
10585 
10586 		logger.close();
10587 	}
10588 	//main;
10589 }
10590 +/
10591 
10592 /+
10593 	=====================
10594 	TRANSLATION FRAMEWORK
10595 	=====================
10596 +/
10597 
10598 /++
10599 	Represents a translatable string.
10600 
10601 
10602 	This depends on interpolated expression sequences to be ergonomic to use and in most cases, a function that uses this should take it as `tstring name...`; a typesafe variadic (this is also why it is a class rather than a struct - D only supports this particular feature on classes).
10603 
10604 	You can use `null` as a tstring. You can also construct it with UFCS: `i"foo".tstring`.
10605 
10606 	The actual translation engine should be set on the application level.
10607 
10608 	It is possible to get all translatable string templates compiled into the application at runtime.
10609 
10610 	History:
10611 		Added June 23, 2024
10612 +/
10613 class tstring {
10614 	private GenericEmbeddableInterpolatedSequence geis;
10615 
10616 	/++
10617 		For a case where there is no plural specialization.
10618 	+/
10619 	this(Args...)(InterpolationHeader hdr, Args args, InterpolationFooter ftr) {
10620 		geis = GenericEmbeddableInterpolatedSequence(hdr, args, ftr);
10621 		tstringTemplateProcessor!(Args.length, Args) tp;
10622 	}
10623 
10624 	/+
10625 	/++
10626 		When here is a plural specialization this passes the default one.
10627 	+/
10628 	this(SArgs..., Pargs...)(
10629 		InterpolationHeader shdr, SArgs singularArgs, InterpolationFooter sftr,
10630 		InterpolationHeader phdr, PArgs pluralArgs, InterpolationFooter pftr
10631 	)
10632 	{
10633 		geis = GenericEmbeddableInterpolatedSequence(shdr, singularArgs, sftr);
10634 		//geis = GenericEmbeddableInterpolatedSequence(phdr, pluralArgs, pftr);
10635 
10636 		tstringTemplateProcessor!(Args.length, Args) tp;
10637 	}
10638 	+/
10639 
10640 	final override string toString() {
10641 		if(this is null)
10642 			return null;
10643 		if(translationEngine !is null)
10644 			return translationEngine.translate(geis);
10645 		else
10646 			return geis.toString();
10647 	}
10648 
10649 	static tstring opCall(Args...)(InterpolationHeader hdr, Args args, InterpolationFooter ftr) {
10650 		return new tstring(hdr, args, ftr);
10651 	}
10652 
10653 	/+ +++ +/
10654 
10655 	private static shared(TranslationEngine) translationEngine_ = null;
10656 
10657 	static shared(TranslationEngine) translationEngine() {
10658 		return translationEngine_;
10659 	}
10660 
10661 	static void translationEngine(shared TranslationEngine e) {
10662 		translationEngine_ = e;
10663 		if(e !is null) {
10664 			auto item = first;
10665 			while(item) {
10666 				e.handleTemplate(*item);
10667 				item = item.next;
10668 			}
10669 		}
10670 	}
10671 
10672 	public struct TranslatableElement {
10673 		string templ;
10674 		string pluralTempl;
10675 
10676 		TranslatableElement* next;
10677 	}
10678 
10679 	static __gshared TranslatableElement* first;
10680 
10681 	// FIXME: the template should be identified to the engine somehow
10682 
10683 	private static enum templateStringFor(Args...) = () {
10684 		int count;
10685 		string templ;
10686 		foreach(arg; Args) {
10687 			static if(is(arg == InterpolatedLiteral!str, string str))
10688 				templ ~= str;
10689 			else static if(is(arg == InterpolatedExpression!code, string code))
10690 				templ ~= "{" ~ cast(char)(++count + '0') ~ "}";
10691 		}
10692 		return templ;
10693 	}();
10694 
10695 	// this is here to inject static ctors so we can build up a runtime list from ct data
10696 	private static struct tstringTemplateProcessor(size_t pluralBegins, Args...) {
10697 		static __gshared TranslatableElement e = TranslatableElement(
10698 			templateStringFor!(Args[0 .. pluralBegins]),
10699 			templateStringFor!(Args[pluralBegins .. $]),
10700 			null /* next, filled in by the static ctor below */);
10701 
10702 		@standalone @system
10703 		shared static this() {
10704 			e.next = first;
10705 			first = &e;
10706 		}
10707 	}
10708 }
10709 
10710 /// ditto
10711 class TranslationEngine {
10712 	string translate(GenericEmbeddableInterpolatedSequence geis) shared {
10713 		return geis.toString();
10714 	}
10715 
10716 	/++
10717 		If the translation engine has been set early in the module
10718 		construction process (which it should be!)
10719 	+/
10720 	void handleTemplate(tstring.TranslatableElement t) shared {
10721 	}
10722 }
10723 
10724 private static template WillFitInGeis(Args...) {
10725 	static int lengthRequired() {
10726 		int place;
10727 		foreach(arg; Args) {
10728 			static if(is(arg == InterpolatedLiteral!str, string str)) {
10729 				if(place & 1) // can't put string in the data slot
10730 					place++;
10731 				place++;
10732 			} else static if(is(arg == InterpolationHeader) || is(arg == InterpolationFooter) || is(arg == InterpolatedExpression!code, string code)) {
10733 				// no storage required
10734 			} else {
10735 				if((place & 1) == 0) // can't put data in the string slot
10736 					place++;
10737 				place++;
10738 			}
10739 		}
10740 
10741 		if(place & 1)
10742 			place++;
10743 		return place / 2;
10744 	}
10745 
10746 	enum WillFitInGeis = lengthRequired() <= GenericEmbeddableInterpolatedSequence.seq.length;
10747 }
10748 
10749 
10750 /+
10751 	For making an array of istrings basically; it moves their CT magic to RT dynamic type.
10752 +/
10753 struct GenericEmbeddableInterpolatedSequence {
10754 	static struct Element {
10755 		string str; // these are pointers to string literals every time
10756 		LimitedVariant lv;
10757 	}
10758 
10759 	Element[8] seq;
10760 
10761 	this(Args...)(InterpolationHeader, Args args, InterpolationFooter) {
10762 		int place;
10763 		bool stringUsedInPlace;
10764 		bool overflowed;
10765 
10766 		static assert(WillFitInGeis!(Args), "Your interpolated elements will not fit in the generic buffer.");
10767 
10768 		foreach(arg; args) {
10769 			static if(is(typeof(arg) == InterpolatedLiteral!str, string str)) {
10770 				if(stringUsedInPlace) {
10771 					place++;
10772 					stringUsedInPlace = false;
10773 				}
10774 
10775 				if(place == seq.length) {
10776 					overflowed = true;
10777 					break;
10778 				}
10779 				seq[place].str = str;
10780 				stringUsedInPlace = true;
10781 			} else static if(is(typeof(arg) == InterpolationHeader) || is(typeof(arg) == InterpolationFooter)) {
10782 				static assert(0, "Cannot embed interpolated sequences");
10783 			} else static if(is(typeof(arg) == InterpolatedExpression!code, string code)) {
10784 				// irrelevant
10785 			} else {
10786 				if(place == seq.length) {
10787 					overflowed = true;
10788 					break;
10789 				}
10790 				seq[place].lv = LimitedVariant(arg);
10791 				place++;
10792 				stringUsedInPlace = false;
10793 			}
10794 		}
10795 	}
10796 
10797 	string toString() {
10798 		string s;
10799 		foreach(item; seq) {
10800 			if(item.str !is null)
10801 				s ~= item.str;
10802 			if(!item.lv.containsNull())
10803 				s ~= item.lv.toString();
10804 		}
10805 		return s;
10806 	}
10807 }
10808 
10809 /+
10810 	=================
10811 	STDIO REPLACEMENT
10812 	=================
10813 +/
10814 
10815 private void appendToBuffer(ref char[] buffer, ref int pos, scope const(char)[] what) {
10816 	auto required = pos + what.length;
10817 	if(buffer.length < required)
10818 		buffer.length = required;
10819 	buffer[pos .. pos + what.length] = what[];
10820 	pos += what.length;
10821 }
10822 
10823 private void appendToBuffer(ref char[] buffer, ref int pos, long what) {
10824 	appendToBuffer(buffer, pos, what, IntToStringArgs.init);
10825 }
10826 private void appendToBuffer(ref char[] buffer, ref int pos, long what, IntToStringArgs args) {
10827 	if(buffer.length < pos + 32)
10828 		buffer.length = pos + 32;
10829 	auto sliced = intToString(what, buffer[pos .. $], args);
10830 	pos += sliced.length;
10831 }
10832 
10833 private void appendToBuffer(ref char[] buffer, ref int pos, double what) {
10834 	appendToBuffer(buffer, pos, what, FloatToStringArgs.init);
10835 }
10836 private void appendToBuffer(ref char[] buffer, ref int pos, double what, FloatToStringArgs args) {
10837 	if(buffer.length < pos + 42)
10838 		buffer.length = pos + 42;
10839 	auto sliced = floatToString(what, buffer[pos .. $], args);
10840 	pos += sliced.length;
10841 }
10842 
10843 
10844 /++
10845 	You can use `mixin(dumpParams);` to put out a debug print of your current function call w/ params.
10846 +/
10847 enum string dumpParams = q{
10848 	{
10849 		import arsd.core;
10850 		arsd.core.dumpParamsImpl(__FUNCTION__, __traits(parameters));
10851 	}
10852 };
10853 
10854 /// Don't call this directly, use `mixin(dumpParams);` instead
10855 public void dumpParamsImpl(T...)(string func, T args) {
10856 	char[256] bufferBacking;
10857 	writeGuts(bufferBacking[], func ~ "(", ")\n", ", ", false, true, &actuallyWriteToStdout, args);
10858 }
10859 
10860 /++
10861 	A `writeln` (and friends) that actually works, at least for some basic types.
10862 
10863 	It works correctly on Windows, using the correct functions to write unicode to the console, even allocating a console if needed. If the output has been redirected to a file or pipe, it writes UTF-8.
10864 
10865 	This always does text. See also WritableStream and WritableTextStream when they are implemented.
10866 +/
10867 void writeln(T...)(T t) {
10868 	char[256] bufferBacking;
10869 	writeGuts(bufferBacking[], null, "\n", null, false, false, &actuallyWriteToStdout, t);
10870 }
10871 
10872 /// ditto
10873 alias writelnStdOut = writeln;
10874 
10875 /// ditto
10876 void writelnStderr(T...)(T t) {
10877 	char[256] bufferBacking;
10878 	writeGuts(bufferBacking[], null, "\n", null, false, false, &actuallyWriteToStderr, t);
10879 }
10880 
10881 /// ditto
10882 void writeStdout(T...)(T t) {
10883 	char[256] bufferBacking;
10884 	writeGuts(bufferBacking[], null, null, null, false, false, &actuallyWriteToStdout, t);
10885 }
10886 
10887 /// ditto
10888 void writeStderr(T...)(T t) {
10889 	char[256] bufferBacking;
10890 	writeGuts(bufferBacking[], null, null, null, false, false, &actuallyWriteToStderr, t);
10891 }
10892 
10893 struct ValueWithFormattingArgs(T : double) {
10894 	double value;
10895 	FloatToStringArgs args;
10896 }
10897 
10898 struct ValueWithFormattingArgs(T : long) {
10899 	long value;
10900 	IntToStringArgs args;
10901 }
10902 
10903 ValueWithFormattingArgs!double formatArgs(double value, FloatToStringArgs args) {
10904 	return ValueWithFormattingArgs!double(value, args);
10905 }
10906 ValueWithFormattingArgs!double formatArgs(double value, int precision) {
10907 	return ValueWithFormattingArgs!double(value, FloatToStringArgs().withPrecision(precision));
10908 
10909 }
10910 
10911 unittest {
10912 	assert(toStringInternal(5.4364.formatArgs(FloatToStringArgs().withPrecision(2))) == "5.44");
10913 	assert(toStringInternal(5.4364.formatArgs(precision: 2)) == "5.44");
10914 }
10915 
10916 /++
10917 
10918 +/
10919 package(arsd) string enumNameForValue(T)(T t) {
10920 	switch(t) {
10921 		foreach(memberName; __traits(allMembers, T)) {
10922 			case __traits(getMember, T, memberName):
10923 				return memberName;
10924 		}
10925 		default:
10926 			return "<unknown>";
10927 	}
10928 }
10929 
10930 /+
10931 	Purposes:
10932 		* debugging
10933 		* writing
10934 		* converting single value to string?
10935 +/
10936 private string writeGuts(T...)(char[] buffer, string prefix, string suffix, string argSeparator, bool printInterpolatedCode, bool quoteStrings, string function(scope char[] result) writer, T t) {
10937 	int pos;
10938 
10939 	if(prefix.length)
10940 		appendToBuffer(buffer, pos, prefix);
10941 
10942 	foreach(i, arg; t) {
10943 		static if(i)
10944 		if(argSeparator.length)
10945 			appendToBuffer(buffer, pos, argSeparator);
10946 
10947 		static if(is(typeof(arg) == InterpolatedExpression!code, string code)) {
10948 			if(printInterpolatedCode) {
10949 				appendToBuffer(buffer, pos, code);
10950 				appendToBuffer(buffer, pos, " = ");
10951 			}
10952 		} else {
10953 			writeIndividualArg(buffer, pos, quoteStrings, arg);
10954 		}
10955 	}
10956 
10957 	if(suffix.length)
10958 		appendToBuffer(buffer, pos, suffix);
10959 
10960 	return writer(buffer[0 .. pos]);
10961 }
10962 
10963 private void writeIndividualArg(T)(ref char[] buffer, ref int pos, bool quoteStrings, T arg) {
10964 	static if(is(typeof(arg) == ValueWithFormattingArgs!V, V)) {
10965 		appendToBuffer(buffer, pos, arg.value, arg.args);
10966 	} else static if(is(typeof(arg) Base == enum)) {
10967 		appendToBuffer(buffer, pos, typeof(arg).stringof);
10968 		appendToBuffer(buffer, pos, ".");
10969 		appendToBuffer(buffer, pos, enumNameForValue(arg));
10970 		appendToBuffer(buffer, pos, "(");
10971 		appendToBuffer(buffer, pos, cast(Base) arg);
10972 		appendToBuffer(buffer, pos, ")");
10973 	} else static if(is(typeof(arg) : const char[])) {
10974 		if(quoteStrings) {
10975 			appendToBuffer(buffer, pos, "\"");
10976 			appendToBuffer(buffer, pos, arg); // FIXME: escape quote and backslash in there?
10977 			appendToBuffer(buffer, pos, "\"");
10978 		} else {
10979 			appendToBuffer(buffer, pos, arg);
10980 		}
10981 	} else static if(is(typeof(arg) : stringz)) {
10982 		appendToBuffer(buffer, pos, arg.borrow);
10983 	} else static if(is(typeof(arg) : long)) {
10984 		appendToBuffer(buffer, pos, arg);
10985 	} else static if(is(typeof(arg) : double)) {
10986 		appendToBuffer(buffer, pos, arg);
10987 	} else static if(is(typeof(arg.toString()) : const char[])) {
10988 		appendToBuffer(buffer, pos, arg.toString());
10989 	} else static if(is(typeof(arg) A == struct)) {
10990 		appendToBuffer(buffer, pos, A.stringof);
10991 		appendToBuffer(buffer, pos, "(");
10992 		foreach(idx, item; arg.tupleof) {
10993 			if(idx)
10994 				appendToBuffer(buffer, pos, ", ");
10995 			appendToBuffer(buffer, pos, __traits(identifier, arg.tupleof[idx]));
10996 			appendToBuffer(buffer, pos, ": ");
10997 			writeIndividualArg(buffer, pos, true, item);
10998 		}
10999 		appendToBuffer(buffer, pos, ")");
11000 	} else static if(is(typeof(arg) == E[], E)) {
11001 		appendToBuffer(buffer, pos, "[");
11002 		foreach(idx, item; arg) {
11003 			if(idx)
11004 				appendToBuffer(buffer, pos, ", ");
11005 			writeIndividualArg(buffer, pos, true, item);
11006 		}
11007 		appendToBuffer(buffer, pos, "]");
11008 	} else static if(is(typeof(arg) == delegate)) {
11009 		appendToBuffer(buffer, pos, "<" ~ typeof(arg).stringof ~ "> ");
11010 		appendToBuffer(buffer, pos, cast(size_t) arg.ptr, IntToStringArgs().withRadix(16).withPadding(12, '0'));
11011 		appendToBuffer(buffer, pos, ", ");
11012 		appendToBuffer(buffer, pos, cast(size_t) arg.funcptr, IntToStringArgs().withRadix(16).withPadding(12, '0'));
11013 	} else static if(is(typeof(arg) : const void*)) {
11014 		appendToBuffer(buffer, pos, "<" ~ typeof(arg).stringof ~ "> ");
11015 		appendToBuffer(buffer, pos, cast(size_t) arg, IntToStringArgs().withRadix(16).withPadding(12, '0'));
11016 	} else {
11017 		appendToBuffer(buffer, pos, "<" ~ typeof(arg).stringof ~ ">");
11018 	}
11019 }
11020 
11021 debug string inspect(T)(T t, string varName = null, int indent = 0) {
11022 	string str;
11023 	foreach(i; 0 .. indent)
11024 		str ~= "\t";
11025 	if(varName.length) {
11026 		str ~= varName;
11027 		str ~= ": ";
11028 	}
11029 	str ~= T.stringof;
11030 	str ~= "(";
11031 	// hack for phobos nullable
11032 	static if(is(T == struct) && __traits(identifier, T) == "Nullable") {
11033 		if(t.isNull) {
11034 			str ~= "null)";
11035 		} else {
11036 			str ~= "\n";
11037 			str ~= inspect(t.get(), null, indent + 1);
11038 			foreach(i; 0 .. indent)
11039 				str ~= "\t";
11040 			str ~= ")";
11041 		}
11042 	}
11043 	else
11044 	// generic inspection
11045 	static if(is(T == class) || is(T == struct) || is(T == interface)) {
11046 		str ~= "\n";
11047 		foreach(memberName; __traits(allMembers, T))
11048 		static if(is(typeof(__traits(getMember, t, memberName).offsetof)))
11049 		{
11050 			str ~= inspect(__traits(getMember, t, memberName), memberName, indent + 1);
11051 		}
11052 		foreach(i; 0 .. indent)
11053 			str ~= "\t";
11054 		str ~= ")";
11055 	} else {
11056 		str ~= toStringInternal(t);
11057 		str ~= ")";
11058 	}
11059 
11060 	str ~= "\n";
11061 
11062 	return str;
11063 }
11064 
11065 debug void dump(T...)(T t, string file = __FILE__, size_t line = __LINE__) {
11066 	string separator;
11067 	static if(T.length && is(T[0] == InterpolationHeader))
11068 		separator = null;
11069 	else
11070 		separator = "; ";
11071 
11072 	char[256] bufferBacking;
11073 	writeGuts(bufferBacking[], file ~ ":" ~ toStringInternal(line) ~ ": ", "\n", separator, true, true, &actuallyWriteToStdout, t);
11074 }
11075 
11076 private string makeString(scope char[] buffer) @safe {
11077 	return buffer.idup;
11078 }
11079 private string makeStringCasting(scope return char[] buffer) @system @nogc nothrow pure {
11080 	return cast(string) buffer;
11081 }
11082 private string actuallyWriteToStdout(scope char[] buffer) @safe {
11083 	return actuallyWriteToStdHandle(1, buffer);
11084 }
11085 private string actuallyWriteToStderr(scope char[] buffer) @safe {
11086 	return actuallyWriteToStdHandle(2, buffer);
11087 }
11088 private string actuallyWriteToStdHandle(int whichOne, scope char[] buffer) @trusted {
11089 	version(UseStdioWriteln)
11090 	{
11091 		import std.stdio;
11092 		(whichOne == 1 ? stdout : stderr).writeln(buffer);
11093 	}
11094 	else version(Windows) {
11095 		import core.sys.windows.wincon;
11096 
11097 		auto h = whichOne == 1 ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE;
11098 
11099 		auto hStdOut = GetStdHandle(h);
11100 		if(hStdOut == null || hStdOut == INVALID_HANDLE_VALUE) {
11101 			AllocConsole();
11102 			hStdOut = GetStdHandle(h);
11103 		}
11104 
11105 		if(GetFileType(hStdOut) == FILE_TYPE_CHAR) {
11106 			WCharzBuffer toWrite = WCharzBuffer(buffer, WindowsStringConversionFlags.convertNewLines);
11107 
11108 			DWORD written;
11109 			WriteConsoleW(hStdOut, toWrite.ptr, cast(DWORD) toWrite.length, &written, null);
11110 		} else {
11111 			DWORD written;
11112 			WriteFile(hStdOut, buffer.ptr, cast(DWORD) buffer.length, &written, null);
11113 		}
11114 	} else {
11115 		import unix = core.sys.posix.unistd;
11116 		unix.write(whichOne, buffer.ptr, buffer.length);
11117 	}
11118 
11119 	return null;
11120 }
11121 
11122 /++
11123 	As the C function it calls, this is not thread safe.
11124 
11125 	Returns:
11126 		`null` if `name` not found. Note this is distinct from an empty string.
11127 +/
11128 string getEnvironmentVariable(scope const(char)[] name) {
11129 	version(Posix) {
11130 		import core.stdc.stdlib;
11131 		CharzBuffer namez = name;
11132 		auto e = getenv(namez.ptr);
11133 		if(e is null)
11134 			return null;
11135 		return stringz(e).borrow.idup;
11136 	} else version(Windows) {
11137 		WCharzBuffer namew = name;
11138 		wchar[128] staticBuffer;
11139 		wchar[] buffer = staticBuffer;
11140 		auto ret = GetEnvironmentVariableW(namew.ptr, buffer.ptr, cast(DWORD) buffer.length);
11141 		if(ret > buffer.length) {
11142 			buffer.length = ret;
11143 			ret = GetEnvironmentVariableW(namew.ptr, buffer.ptr, cast(DWORD) buffer.length);
11144 		}
11145 		if(ret == 0) {
11146 			auto err = GetLastError();
11147 			if(err == ERROR_SUCCESS) {
11148 				return "";
11149 			} else if(err == ERROR_ENVVAR_NOT_FOUND) {
11150 				return null;
11151 			} else {
11152 				throw new WindowsApiException("GetEnvironmentVariable", err);
11153 			}
11154 		}
11155 
11156 		return makeUtf8StringFromWindowsString(buffer[0 .. ret]);
11157 	} else static assert(0);
11158 }
11159 
11160 /+
11161 
11162 STDIO
11163 
11164 	/++
11165 		Please note using this will create a compile-time dependency on [arsd.terminal]
11166 
11167 
11168 
11169 so my writeln replacement:
11170 
11171 1) if the std output handle is null, alloc one
11172 2) if it is a character device, write out the proper Unicode text.
11173 3) otherwise write out UTF-8.... maybe with a BOM but maybe not. it is tricky to know what the other end of a pipe expects...
11174 [8:15 AM]
11175 im actually tempted to make the write binary to stdout functions throw an exception if it is a character console / interactive terminal instead of letting you spam it right out
11176 [8:16 AM]
11177 of course you can still cheat by casting binary data to string and using the write string function (and this might be appropriate sometimes) but there kinda is a legit difference between a text output and a binary output device
11178 
11179 Stdout can represent either
11180 
11181 	+/
11182 	void writeln(){} {
11183 
11184 	}
11185 
11186 	stderr?
11187 
11188 	/++
11189 		Please note using this will create a compile-time dependency on [arsd.terminal]
11190 
11191 		It can be called from a task.
11192 
11193 		It works correctly on Windows and is user friendly on Linux (using arsd.terminal.getline)
11194 		while also working if stdin has been redirected (where arsd.terminal itself would throw)
11195 
11196 
11197 so say you run a program on an interactive terminal. the program tries to open the stdin binary stream
11198 
11199 instead of throwing, the prompt could change to indicate the binary data is expected and you can feed it in either by typing it up,,,,  or running some command like maybe <file.bin to have the library do what the shell would have done and feed that to the rest of the program
11200 
11201 	+/
11202 	string readln()() {
11203 
11204 	}
11205 
11206 
11207 	// if using stdio as a binary output thing you can pretend it is a file w/ stream capability
11208 	struct File {
11209 		WritableStream ostream;
11210 		ReadableStream istream;
11211 
11212 		ulong tell;
11213 		void seek(ulong to) {}
11214 
11215 		void sync();
11216 		void close();
11217 	}
11218 
11219 	// these are a bit special because if it actually is an interactive character device, it might be different than other files and even different than other pipes.
11220 	WritableStream stdoutStream() { return null; }
11221 	WritableStream stderrStream() { return null; }
11222 	ReadableStream stdinStream() { return null; }
11223 
11224 +/
11225 
11226 
11227 /+
11228 
11229 
11230 /+
11231 	Druntime appears to have stuff for darwin, freebsd. I might have to add some for openbsd here and maybe netbsd if i care to test it.
11232 +/
11233 
11234 /+
11235 
11236 	arsd_core_init(number_of_worker_threads)
11237 
11238 	Building-block things wanted for the event loop integration:
11239 		* ui
11240 			* windows
11241 			* terminal / console
11242 		* generic
11243 			* adopt fd
11244 			* adopt windows handle
11245 		* shared lib
11246 			* load
11247 		* timers (relative and real time)
11248 			* create
11249 			* update
11250 			* cancel
11251 		* file/directory watches
11252 			* file created
11253 			* file deleted
11254 			* file modified
11255 		* file ops
11256 			* open
11257 			* close
11258 			* read
11259 			* write
11260 			* seek
11261 			* sendfile on linux, TransmitFile on Windows
11262 			* let completion handlers run in the io worker thread instead of signaling back
11263 		* pipe ops (anonymous or named)
11264 			* create
11265 			* read
11266 			* write
11267 			* get info about other side of the pipe
11268 		* network ops (stream + datagram, ip, ipv6, unix)
11269 			* address look up
11270 			* connect
11271 			* start tls
11272 			* listen
11273 			* send
11274 			* receive
11275 			* get peer info
11276 		* process ops
11277 			* spawn
11278 			* notifications when it is terminated or fork or execs
11279 			* send signal
11280 			* i/o pipes
11281 		* thread ops (isDaemon?)
11282 			* spawn
11283 			* talk to its event loop
11284 			* termination notification
11285 		* signals
11286 			* ctrl+c is the only one i really care about but the others might be made available too. sigchld needs to be done as an impl detail of process ops.
11287 		* custom messages
11288 			* should be able to send messages from finalizers...
11289 
11290 		* want to make sure i can stream stuff on top of it all too.
11291 
11292 		========
11293 
11294 		These things all refer back to a task-local thing that queues the tasks. If it is a fiber, it uses that
11295 		and if it is a thread it uses that...
11296 
11297 		tls IArsdCoreEventLoop curentTaskInterface; // this yields on the wait for calls. the fiber swapper will swap this too.
11298 		tls IArsdCoreEventLoop currentThreadInterface; // this blocks on the event loop
11299 
11300 		shared IArsdCoreEventLoop currentProcessInterface; // this dispatches to any available thread
11301 +/
11302 
11303 
11304 /+
11305 	You might have configurable tasks that do not auto-start, e.g. httprequest. maybe @mustUse on those
11306 
11307 	then some that do auto-start, e.g. setTimeout
11308 
11309 
11310 	timeouts: duration, MonoTime, or SysTime? duration is just a timer monotime auto-adjusts the when, systime sets a real time timerfd
11311 
11312 	tasks can be set to:
11313 		thread affinity - this, any, specific reference
11314 		reports to - defaults to this, can also pass down a parent reference. if reports to dies, all its subordinates are cancelled.
11315 
11316 
11317 	you can send a message to a task... maybe maybe just to a task runner (which is itself a task?)
11318 
11319 	auto file = readFile(x);
11320 	auto timeout = setTimeout(y);
11321 	auto completed = waitForFirstToCompleteThenCancelOthers(file, timeout);
11322 	if(completed == 0) {
11323 		file....
11324 	} else {
11325 		timeout....
11326 	}
11327 
11328 	/+
11329 		A task will run on a thread (with possible migration), and report to a task.
11330 	+/
11331 
11332 	// a compute task is run on a helper thread
11333 	auto task = computeTask((shared(bool)* cancellationRequested) {
11334 		// or pass in a yield thing... prolly a TaskController which has cancellationRequested and yield controls as well as send message to parent (sync or async)
11335 
11336 		// you'd periodically send messages back to the parent
11337 	}, RunOn.AnyAvailable, Affinity.CanMigrate);
11338 
11339 	auto task = Task((TaskController controller) {
11340 		foreach(x, 0 .. 1000) {
11341 			if(x % 10 == 0)
11342 				controller.yield(); // periodically yield control, which also checks for cancellation for us
11343 			// do some work
11344 
11345 			controller.sendMessage(...);
11346 			controller.sendProgress(x); // yields it for a foreach stream kind of thing
11347 		}
11348 
11349 		return something; // automatically sends the something as the result in a TaskFinished message
11350 	});
11351 
11352 	foreach(item; task) // waitsForProgress, sendProgress sends an item and the final return sends an item
11353 		{}
11354 
11355 
11356 		see ~/test/task.d
11357 
11358 	// an io task is run locally via the event loops
11359 	auto task2 = ioTask(() {
11360 
11361 	});
11362 
11363 
11364 
11365 	waitForEvent
11366 +/
11367 
11368 /+
11369 	Most functions should prolly take a thread arg too, which defaults
11370 	to this thread, but you can also pass it a reference, or a "any available" thing.
11371 
11372 	This can be a ufcs overload
11373 +/
11374 
11375 interface SemiSynchronousTask {
11376 
11377 }
11378 
11379 struct TimeoutCompletionResult {
11380 	bool completed;
11381 
11382 	bool opCast(T : bool)() {
11383 		return completed;
11384 	}
11385 }
11386 
11387 struct Timeout {
11388 	void reschedule(Duration when) {
11389 
11390 	}
11391 
11392 	void cancel() {
11393 
11394 	}
11395 
11396 	TimeoutCompletionResult waitForCompletion() {
11397 		return TimeoutCompletionResult(false);
11398 	}
11399 }
11400 
11401 Timeout setTimeout(void delegate() dg, int msecs, int permittedJitter = 20) {
11402 static assert(0);
11403 	return Timeout.init;
11404 }
11405 
11406 void clearTimeout(Timeout timeout) {
11407 	timeout.cancel();
11408 }
11409 
11410 void createInterval() {}
11411 void clearInterval() {}
11412 
11413 /++
11414 	Schedules a task at the given wall clock time.
11415 +/
11416 void scheduleTask() {}
11417 
11418 struct IoOperationCompletionResult {
11419 	enum Status {
11420 		cancelled,
11421 		completed
11422 	}
11423 
11424 	Status status;
11425 
11426 	int error;
11427 	int bytesWritten;
11428 
11429 	bool opCast(T : bool)() {
11430 		return status == Status.completed;
11431 	}
11432 }
11433 
11434 struct IoOperation {
11435 	void cancel() {}
11436 
11437 	IoOperationCompletionResult waitForCompletion() {
11438 		return IoOperationCompletionResult.init;
11439 	}
11440 
11441 	// could contain a scoped class in here too so it stack allocated
11442 }
11443 
11444 // Should return both the object and the index in the array!
11445 Result waitForFirstToComplete(Operation[]...) {}
11446 
11447 IoOperation read(IoHandle handle, ubyte[] buffer
11448 
11449 /+
11450 	class IoOperation {}
11451 
11452 	// an io operation and its buffer must not be modified or freed
11453 	// in between a call to enqueue and a call to waitForCompletion
11454 	// if you used the whenComplete callback, make sure it is NOT gc'd or scope thing goes out of scope in the mean time
11455 	// if its dtor runs, it'd be forced to be cancelled...
11456 
11457 	scope IoOperation op = new IoOperation(buffer_size);
11458 	op.start();
11459 	op.waitForCompletion();
11460 +/
11461 
11462 /+
11463 	will want:
11464 		read, write
11465 		send, recv
11466 
11467 		cancel
11468 
11469 		open file, open (named or anonymous) pipe, open process
11470 		connect, accept
11471 		SSL
11472 		close
11473 
11474 		postEvent
11475 		postAPC? like run in gui thread / async
11476 		waitForEvent ? needs to handle a timeout and a cancellation. would only work in the fiber task api.
11477 
11478 		waitForSuccess
11479 
11480 		interrupt handler
11481 
11482 		onPosixReadReadiness
11483 		onPosixWriteReadiness
11484 
11485 		onWindowsHandleReadiness
11486 			- but they're one-offs so you gotta reregister for each event
11487 +/
11488 
11489 
11490 
11491 /+
11492 arsd.core.uda
11493 
11494 you define a model struct with the types you want to extract
11495 
11496 you get it with like Model extract(Model, UDAs...)(Model default)
11497 
11498 defaultModel!alias > defaultModel!Type(defaultModel("identifier"))
11499 
11500 
11501 
11502 
11503 
11504 
11505 
11506 
11507 
11508 
11509 so while i laid there sleep deprived i did think a lil more on some uda stuff. it isn't especially novel but a combination of a few other techniques
11510 
11511 you might be like
11512 
11513 struct MyUdas {
11514     DbName name;
11515     DbIgnore ignore;
11516 }
11517 
11518 elsewhere
11519 
11520 foreach(alias; allMembers) {
11521      auto udas = getUdas!(MyUdas, __traits(getAttributes, alias))(MyUdas(DbName(__traits(identifier, alias))));
11522 }
11523 
11524 
11525 so you pass the expected type and the attributes as the template params, then the runtime params are the default values for the given types
11526 
11527 so what the thing does essentially is just sets the values of the given thing to the udas based on type then returns the modified instance
11528 
11529 so the end result is you keep the last ones. it wouldn't report errors if multiple things added but it p simple to understand, simple to document (even though the default values are not in the struct itself, you can put ddocs in them), and uses the tricks to minimize generated code size
11530 +/
11531 
11532 +/
11533 
11534 package(arsd) version(Windows) extern(Windows) {
11535 	BOOL CancelIoEx(HANDLE, LPOVERLAPPED);
11536 
11537 	struct WSABUF {
11538 		ULONG len;
11539 		ubyte* buf;
11540 	}
11541 	alias LPWSABUF = WSABUF*;
11542 
11543 	// https://learn.microsoft.com/en-us/windows/win32/api/winsock2/ns-winsock2-wsaoverlapped
11544 	// "The WSAOVERLAPPED structure is compatible with the Windows OVERLAPPED structure."
11545 	// so ima lie here in the bindings.
11546 
11547 	int WSASend(SOCKET, LPWSABUF, DWORD, LPDWORD, DWORD, LPOVERLAPPED, LPOVERLAPPED_COMPLETION_ROUTINE);
11548 	int WSASendTo(SOCKET, LPWSABUF, DWORD, LPDWORD, DWORD, const sockaddr*, int, LPOVERLAPPED, LPOVERLAPPED_COMPLETION_ROUTINE);
11549 
11550 	int WSARecv(SOCKET, LPWSABUF, DWORD, LPDWORD, LPDWORD, LPOVERLAPPED, LPOVERLAPPED_COMPLETION_ROUTINE);
11551 	int WSARecvFrom(SOCKET, LPWSABUF, DWORD, LPDWORD, LPDWORD, sockaddr*, LPINT, LPOVERLAPPED, LPOVERLAPPED_COMPLETION_ROUTINE);
11552 }
11553 
11554 package(arsd) version(UseCocoa) {
11555 
11556 /* Copy/paste chunk from Jacob Carlborg { */
11557 // from https://raw.githubusercontent.com/jacob-carlborg/druntime/550edd0a64f0eb2c4f35d3ec3d88e26b40ac779e/src/core/stdc/clang_block.d
11558 // with comments stripped (see docs in the original link), code reformatted, and some names changed to avoid potential conflicts
11559 
11560 // note these should always be passed by pointer!
11561 
11562 import core.stdc.config;
11563 struct ObjCBlock(R = void, Params...) {
11564 private:
11565 	alias extern(C) R function(ObjCBlock*, Params) Invoke;
11566 
11567 	void* isa;
11568 	int flags;
11569 	int reserved = 0;
11570 	Invoke invoke;
11571 	Descriptor* descriptor;
11572 
11573 	// Imported variables go here
11574 	R delegate(Params) dg;
11575 
11576 	this(void* isa, int flags, Invoke invoke, R delegate(Params) dg) {
11577 		this.isa = isa;
11578 		this.flags = flags;
11579 		this.invoke = invoke;
11580 		this.descriptor = &.objcblock_descriptor;
11581 
11582 		// FIXME: is this needed or not? it could be held by the OS and not be visible to GC i think
11583 		// import core.memory; GC.addRoot(dg.ptr);
11584 
11585 		this.dg = dg;
11586 	}
11587 }
11588 ObjCBlock!(R, Params) blockOnStack(R, Params...)(R delegate(Params) dg) {
11589 	static if (Params.length == 0)
11590 		enum flags = 0x50000000;
11591 	else
11592 		enum flags = 0x40000000;
11593 
11594 	return ObjCBlock!(R, Params)(&_NSConcreteStackBlock, flags, &objcblock_invoke!(R, Params), dg);
11595 }
11596 ObjCBlock!(R, Params)* block(R, Params...)(R delegate(Params) dg) {
11597 	static if (Params.length == 0)
11598 		enum flags = 0x50000000;
11599 	else
11600 		enum flags = 0x40000000;
11601 
11602 	return new ObjCBlock!(R, Params)(&_NSConcreteStackBlock, flags, &objcblock_invoke!(R, Params), dg);
11603 }
11604 
11605 private struct Descriptor {
11606     c_ulong reserved;
11607     c_ulong size;
11608     const(char)* signature;
11609 }
11610 private extern(C) extern __gshared void*[32] _NSConcreteStackBlock;
11611 private __gshared auto objcblock_descriptor = Descriptor(0, ObjCBlock!().sizeof);
11612 private extern(C) R objcblock_invoke(R, Args...)(ObjCBlock!(R, Args)* block, Args args) {
11613     return block.dg(args);
11614 }
11615 
11616 
11617 /* End copy/paste chunk from Jacob Carlborg } */
11618 
11619 
11620 /+
11621 To let Cocoa know that you intend to use multiple threads, all you have to do is spawn a single thread using the NSThread class and let that thread immediately exit. Your thread entry point need not do anything. Just the act of spawning a thread using NSThread is enough to ensure that the locks needed by the Cocoa frameworks are put in place.
11622 
11623 If you are not sure if Cocoa thinks your application is multithreaded or not, you can use the isMultiThreaded method of NSThread to check.
11624 +/
11625 
11626 
11627 	struct DeifiedNSString {
11628 		char[16] sso;
11629 		const(char)[] str;
11630 
11631 		this(NSString s) {
11632 			auto len = s.length;
11633 			if(len <= sso.length / 4)
11634 				str = sso[];
11635 			else
11636 				str = new char[](len * 4);
11637 
11638 			NSUInteger count;
11639 			NSRange leftover;
11640 			auto ret = s.getBytes(cast(char*) str.ptr, str.length, &count, NSStringEncoding.NSUTF8StringEncoding, NSStringEncodingConversionOptions.none, NSRange(0, len), &leftover);
11641 			if(ret)
11642 				str = str[0 .. count];
11643 			else
11644 				throw new Exception("uh oh");
11645 		}
11646 	}
11647 
11648 	extern (Objective-C) {
11649 		import core.attribute; // : selector, optional;
11650 
11651 		alias NSUInteger = size_t;
11652 		alias NSInteger = ptrdiff_t;
11653 		alias unichar = wchar;
11654 		struct SEL_;
11655 		alias SEL_* SEL;
11656 		// this is called plain `id` in objective C but i fear mistakes with that in D. like sure it is a type instead of a variable like most things called id but i still think it is weird. i might change my mind later.
11657 		alias void* NSid; // FIXME? the docs say this is a pointer to an instance of a class, but that is not necessary a child of NSObject
11658 
11659 		extern class NSObject {
11660 			static NSObject alloc() @selector("alloc");
11661 			NSObject init() @selector("init");
11662 
11663 			void retain() @selector("retain");
11664 			void release() @selector("release");
11665 			void autorelease() @selector("autorelease");
11666 
11667 			void performSelectorOnMainThread(SEL aSelector, NSid arg, bool waitUntilDone) @selector("performSelectorOnMainThread:withObject:waitUntilDone:");
11668 		}
11669 
11670 		// this is some kind of generic in objc...
11671 		extern class NSArray : NSObject {
11672 			static NSArray arrayWithObjects(NSid* objects, NSUInteger count) @selector("arrayWithObjects:count:");
11673 		}
11674 
11675 		extern class NSString : NSObject {
11676 			override static NSString alloc() @selector("alloc");
11677 			override NSString init() @selector("init");
11678 
11679 			NSString initWithUTF8String(const scope char* str) @selector("initWithUTF8String:");
11680 
11681 			NSString initWithBytes(
11682 				const(ubyte)* bytes,
11683 				NSUInteger length,
11684 				NSStringEncoding encoding
11685 			) @selector("initWithBytes:length:encoding:");
11686 
11687 			unichar characterAtIndex(NSUInteger index) @selector("characterAtIndex:");
11688 			NSUInteger length() @selector("length");
11689 			const char* UTF8String() @selector("UTF8String");
11690 
11691 			void getCharacters(wchar* buffer, NSRange range) @selector("getCharacters:range:");
11692 
11693 			bool getBytes(void* buffer, NSUInteger maxBufferCount, NSUInteger* usedBufferCount, NSStringEncoding encoding, NSStringEncodingConversionOptions options, NSRange range, NSRange* leftover) @selector("getBytes:maxLength:usedLength:encoding:options:range:remainingRange:");
11694 
11695 			CGSize sizeWithAttributes(NSDictionary attrs) @selector("sizeWithAttributes:");
11696 		}
11697 
11698 		// FIXME: it is a generic in objc with <KeyType, ObjectType>
11699 		extern class NSDictionary : NSObject {
11700 			static NSDictionary dictionaryWithObject(NSObject object, NSid key) @selector("dictionaryWithObject:forKey:");
11701 			// static NSDictionary initWithObjects(NSArray objects, NSArray forKeys) @selector("initWithObjects:forKeys:");
11702 		}
11703 
11704 		alias NSAttributedStringKey = NSString;
11705 		/* const */extern __gshared NSAttributedStringKey NSFontAttributeName;
11706 
11707 		struct NSRange {
11708 			NSUInteger loc;
11709 			NSUInteger len;
11710 		}
11711 
11712 		enum NSStringEncodingConversionOptions : NSInteger {
11713 			none = 0,
11714 			NSAllowLossyEncodingConversion = 1,
11715 			NSExternalRepresentationEncodingConversion = 2
11716 		}
11717 
11718 		enum NSEventType {
11719 			idk
11720 
11721 		}
11722 
11723 		enum NSEventModifierFlags : NSUInteger {
11724 			NSEventModifierFlagCapsLock = 1 << 16,
11725 			NSEventModifierFlagShift = 1 << 17,
11726 			NSEventModifierFlagControl = 1 << 18,
11727 			NSEventModifierFlagOption = 1 << 19, // aka Alt
11728 			NSEventModifierFlagCommand = 1 << 20, // aka super
11729 			NSEventModifierFlagNumericPad = 1 << 21,
11730 			NSEventModifierFlagHelp = 1 << 22,
11731 			NSEventModifierFlagFunction = 1 << 23,
11732 			NSEventModifierFlagDeviceIndependentFlagsMask = 0xffff0000UL
11733 		}
11734 
11735 		version(OSX)
11736 		extern class NSEvent : NSObject {
11737 			NSEventType type() @selector("type");
11738 
11739 			NSPoint locationInWindow() @selector("locationInWindow");
11740 			NSTimeInterval timestamp() @selector("timestamp");
11741 			NSWindow window() @selector("window"); // note: nullable
11742 			NSEventModifierFlags modifierFlags() @selector("modifierFlags");
11743 
11744 			NSString characters() @selector("characters");
11745 			NSString charactersIgnoringModifiers() @selector("charactersIgnoringModifiers");
11746 			ushort keyCode() @selector("keyCode");
11747 			ushort specialKey() @selector("specialKey");
11748 
11749 			static NSUInteger pressedMouseButtons() @selector("pressedMouseButtons");
11750 			NSPoint locationInWindow() @selector("locationInWindow"); // in screen coordinates
11751 			static NSPoint mouseLocation() @selector("mouseLocation"); // in screen coordinates
11752 			NSInteger buttonNumber() @selector("buttonNumber");
11753 
11754 			CGFloat deltaX() @selector("deltaX");
11755 			CGFloat deltaY() @selector("deltaY");
11756 			CGFloat deltaZ() @selector("deltaZ");
11757 
11758 			bool hasPreciseScrollingDeltas() @selector("hasPreciseScrollingDeltas");
11759 
11760 			CGFloat scrollingDeltaX() @selector("scrollingDeltaX");
11761 			CGFloat scrollingDeltaY() @selector("scrollingDeltaY");
11762 
11763 			// @property(getter=isDirectionInvertedFromDevice, readonly) BOOL directionInvertedFromDevice;
11764 		}
11765 
11766 		extern /* final */ class NSTimer : NSObject { // the docs say don't subclass this, but making it final breaks the bridge
11767 			override static NSTimer alloc() @selector("alloc");
11768 			override NSTimer init() @selector("init");
11769 
11770 			static NSTimer schedule(NSTimeInterval timeIntervalInSeconds, NSid target, SEL selector, NSid userInfo, bool repeats) @selector("scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:");
11771 
11772 			void fire() @selector("fire");
11773 			void invalidate() @selector("invalidate");
11774 
11775 			bool valid() @selector("isValid");
11776 			// @property(copy) NSDate *fireDate;
11777 			NSTimeInterval timeInterval() @selector("timeInterval");
11778 			NSid userInfo() @selector("userInfo");
11779 
11780 			NSTimeInterval tolerance() @selector("tolerance");
11781 			NSTimeInterval tolerance(NSTimeInterval) @selector("setTolerance:");
11782 		}
11783 
11784 		alias NSTimeInterval = double;
11785 
11786 		version(OSX)
11787 		extern class NSResponder : NSObject {
11788 			NSMenu menu() @selector("menu");
11789 			void menu(NSMenu menu) @selector("setMenu:");
11790 
11791 			void keyDown(NSEvent event) @selector("keyDown:");
11792 			void keyUp(NSEvent event) @selector("keyUp:");
11793 
11794 			// - (void)interpretKeyEvents:(NSArray<NSEvent *> *)eventArray;
11795 
11796 			void mouseDown(NSEvent event) @selector("mouseDown:");
11797 			void mouseDragged(NSEvent event) @selector("mouseDragged:");
11798 			void mouseUp(NSEvent event) @selector("mouseUp:");
11799 			void mouseMoved(NSEvent event) @selector("mouseMoved:");
11800 			void mouseEntered(NSEvent event) @selector("mouseEntered:");
11801 			void mouseExited(NSEvent event) @selector("mouseExited:");
11802 
11803 			void rightMouseDown(NSEvent event) @selector("rightMouseDown:");
11804 			void rightMouseDragged(NSEvent event) @selector("rightMouseDragged:");
11805 			void rightMouseUp(NSEvent event) @selector("rightMouseUp:");
11806 
11807 			void otherMouseDown(NSEvent event) @selector("otherMouseDown:");
11808 			void otherMouseDragged(NSEvent event) @selector("otherMouseDragged:");
11809 			void otherMouseUp(NSEvent event) @selector("otherMouseUp:");
11810 
11811 			void scrollWheel(NSEvent event) @selector("scrollWheel:");
11812 
11813 			// touch events should also be here btw among others
11814 		}
11815 
11816 		version(OSX)
11817 		extern class NSApplication : NSResponder {
11818 			static NSApplication shared_() @selector("sharedApplication");
11819 
11820 			NSApplicationDelegate delegate_() @selector("delegate");
11821 			void delegate_(NSApplicationDelegate) @selector("setDelegate:");
11822 
11823 			bool setActivationPolicy(NSApplicationActivationPolicy activationPolicy) @selector("setActivationPolicy:");
11824 
11825 			void activateIgnoringOtherApps(bool flag) @selector("activateIgnoringOtherApps:");
11826 
11827 			@property NSMenu mainMenu() @selector("mainMenu");
11828 			@property NSMenu mainMenu(NSMenu) @selector("setMainMenu:");
11829 
11830 			void run() @selector("run");
11831 
11832 			void stop(NSid sender) @selector("stop:");
11833 
11834 			void finishLaunching() @selector("finishLaunching");
11835 
11836 			void terminate(void*) @selector("terminate:");
11837 
11838 			void sendEvent(NSEvent event) @selector("sendEvent:");
11839 			NSEvent nextEventMatchingMask(
11840 				NSEventMask mask,
11841 				NSDate untilDate,
11842 				NSRunLoopMode inMode,
11843 				bool dequeue
11844 			) @selector("nextEventMatchingMask:untilDate:inMode:dequeue:");
11845 		}
11846 
11847 		enum NSEventMask : ulong {
11848 			NSEventMaskAny = ulong.max
11849 		}
11850 
11851 		version(OSX)
11852 		extern class NSRunLoop : NSObject {
11853 			static @property NSRunLoop currentRunLoop() @selector("currentRunLoop");
11854 			static @property NSRunLoop mainRunLoop() @selector("mainRunLoop");
11855 			bool runMode(NSRunLoopMode mode, NSDate beforeDate) @selector("runMode:beforeDate:");
11856 		}
11857 
11858 		alias NSRunLoopMode = NSString;
11859 
11860 		extern __gshared NSRunLoopMode NSDefaultRunLoopMode;
11861 
11862 		version(OSX)
11863 		extern class NSDate : NSObject {
11864 			static @property NSDate distantFuture() @selector("distantFuture");
11865 			static @property NSDate distantPast() @selector("distantPast");
11866 			static @property NSDate now() @selector("now");
11867 
11868 		}
11869 
11870 		version(OSX)
11871 		extern interface NSApplicationDelegate {
11872 			void applicationWillFinishLaunching(NSNotification notification) @selector("applicationWillFinishLaunching:");
11873 			void applicationDidFinishLaunching(NSNotification notification) @selector("applicationDidFinishLaunching:");
11874 			bool applicationShouldTerminateAfterLastWindowClosed(NSNotification notification) @selector("applicationShouldTerminateAfterLastWindowClosed:");
11875 		}
11876 
11877 		extern class NSNotification : NSObject {
11878 			@property NSid object() @selector("object");
11879 		}
11880 
11881 		enum NSApplicationActivationPolicy : ptrdiff_t {
11882 			/* The application is an ordinary app that appears in the Dock and may have a user interface.  This is the default for bundled apps, unless overridden in the Info.plist. */
11883 			regular,
11884 
11885 			/* The application does not appear in the Dock and does not have a menu bar, but it may be activated programmatically or by clicking on one of its windows.  This corresponds to LSUIElement=1 in the Info.plist. */
11886 			accessory,
11887 
11888 			/* The application does not appear in the Dock and may not create windows or be activated.  This corresponds to LSBackgroundOnly=1 in the Info.plist.  This is also the default for unbundled executables that do not have Info.plists. */
11889 			prohibited
11890 		}
11891 
11892 		extern class NSGraphicsContext : NSObject {
11893 			static NSGraphicsContext currentContext() @selector("currentContext");
11894 			NSGraphicsContext graphicsPort() @selector("graphicsPort");
11895 		}
11896 
11897 		version(OSX)
11898 		extern class NSMenu : NSObject {
11899 			override static NSMenu alloc() @selector("alloc");
11900 
11901 			override NSMenu init() @selector("init");
11902 			NSMenu init(NSString title) @selector("initWithTitle:");
11903 
11904 			void setSubmenu(NSMenu menu, NSMenuItem item) @selector("setSubmenu:forItem:");
11905 			void addItem(NSMenuItem newItem) @selector("addItem:");
11906 
11907 			NSMenuItem addItem(
11908 				NSString title,
11909 				SEL selector,
11910 				NSString charCode
11911 			) @selector("addItemWithTitle:action:keyEquivalent:");
11912 		}
11913 
11914 		version(OSX)
11915 		extern class NSMenuItem : NSObject {
11916 			override static NSMenuItem alloc() @selector("alloc");
11917 			override NSMenuItem init() @selector("init");
11918 
11919 			NSMenuItem init(
11920 				NSString title,
11921 				SEL selector,
11922 				NSString charCode
11923 			) @selector("initWithTitle:action:keyEquivalent:");
11924 
11925 			void enabled(bool) @selector("setEnabled:");
11926 
11927 			NSResponder target(NSResponder) @selector("setTarget:");
11928 		}
11929 
11930 		enum NSWindowStyleMask : size_t {
11931 			borderless = 0,
11932 			titled = 1 << 0,
11933 			closable = 1 << 1,
11934 			miniaturizable = 1 << 2,
11935 			resizable	= 1 << 3,
11936 
11937 			/* Specifies a window with textured background. Textured windows generally don't draw a top border line under the titlebar/toolbar. To get that line, use the NSUnifiedTitleAndToolbarWindowMask mask.
11938 			 */
11939 			texturedBackground = 1 << 8,
11940 
11941 			/* Specifies a window whose titlebar and toolbar have a unified look - that is, a continuous background. Under the titlebar and toolbar a horizontal separator line will appear.
11942 			 */
11943 			unifiedTitleAndToolbar = 1 << 12,
11944 
11945 			/* When set, the window will appear full screen. This mask is automatically toggled when toggleFullScreen: is called.
11946 			 */
11947 			fullScreen = 1 << 14,
11948 
11949 			/* If set, the contentView will consume the full size of the window; it can be combined with other window style masks, but is only respected for windows with a titlebar.
11950 			 Utilizing this mask opts-in to layer-backing. Utilize the contentLayoutRect or auto-layout contentLayoutGuide to layout views underneath the titlebar/toolbar area.
11951 			 */
11952 			fullSizeContentView = 1 << 15,
11953 
11954 			/* The following are only applicable for NSPanel (or a subclass thereof)
11955 			 */
11956 			utilityWindow			= 1 << 4,
11957 			docModalWindow		 = 1 << 6,
11958 			nonactivatingPanel		= 1 << 7, // Specifies that a panel that does not activate the owning application
11959 			hUDWindow = 1 << 13 // Specifies a heads up display panel
11960 		}
11961 
11962 		version(OSX)
11963 		extern class NSWindow : NSObject {
11964 			override static NSWindow alloc() @selector("alloc");
11965 
11966 			override NSWindow init() @selector("init");
11967 
11968 			NSWindow initWithContentRect(
11969 				NSRect contentRect,
11970 				NSWindowStyleMask style,
11971 				NSBackingStoreType bufferingType,
11972 				bool flag
11973 			) @selector("initWithContentRect:styleMask:backing:defer:");
11974 
11975 			void makeKeyAndOrderFront(NSid sender) @selector("makeKeyAndOrderFront:");
11976 			NSView contentView() @selector("contentView");
11977 			void contentView(NSView view) @selector("setContentView:");
11978 			void orderFrontRegardless() @selector("orderFrontRegardless");
11979 			void center() @selector("center");
11980 
11981 			NSRect frame() @selector("frame");
11982 
11983 			NSRect contentRectForFrameRect(NSRect frameRect) @selector("contentRectForFrameRect:");
11984 			NSRect frameRectForContentRect(NSRect contentRect) @selector("frameRectForContentRect:");
11985 
11986 			NSString title() @selector("title");
11987 			void title(NSString value) @selector("setTitle:");
11988 
11989 			void close() @selector("close");
11990 
11991 			NSWindowDelegate delegate_() @selector("delegate");
11992 			void delegate_(NSWindowDelegate) @selector("setDelegate:");
11993 
11994 			void setBackgroundColor(NSColor color) @selector("setBackgroundColor:");
11995 
11996 			void setIsVisible(bool b) @selector("setIsVisible:");
11997 		}
11998 
11999 		version(OSX)
12000 		extern interface NSWindowDelegate {
12001 			@optional:
12002 			void windowDidResize(NSNotification notification) @selector("windowDidResize:");
12003 
12004 			NSSize windowWillResize(NSWindow sender, NSSize frameSize) @selector("windowWillResize:toSize:");
12005 
12006 			void windowWillClose(NSNotification notification) @selector("windowWillClose:");
12007 		}
12008 
12009 		version(OSX)
12010 		extern class NSView : NSResponder {
12011 			//override NSView init() @selector("init");
12012 			NSView initWithFrame(NSRect frameRect) @selector("initWithFrame:");
12013 
12014 			void addSubview(NSView view) @selector("addSubview:");
12015 
12016 			bool wantsLayer() @selector("wantsLayer");
12017 			void wantsLayer(bool value) @selector("setWantsLayer:");
12018 
12019 			CALayer layer() @selector("layer");
12020 			void uiDelegate(NSObject) @selector("setUIDelegate:");
12021 
12022 			void drawRect(NSRect rect) @selector("drawRect:");
12023 			bool isFlipped() @selector("isFlipped");
12024 			bool acceptsFirstResponder() @selector("acceptsFirstResponder");
12025 			bool setNeedsDisplay(bool) @selector("setNeedsDisplay:");
12026 
12027 			// DO NOT USE: https://issues.dlang.org/show_bug.cgi?id=19017
12028 			// an asm { pop RAX; } after getting the struct can kinda hack around this but still
12029 			@property NSRect frame() @selector("frame");
12030 			@property NSRect frame(NSRect rect) @selector("setFrame:");
12031 
12032 			void setFrameSize(NSSize newSize) @selector("setFrameSize:");
12033 			void setFrameOrigin(NSPoint newOrigin) @selector("setFrameOrigin:");
12034 
12035 			void addSubview(NSView what) @selector("addSubview:");
12036 			void removeFromSuperview() @selector("removeFromSuperview");
12037 		}
12038 
12039 		extern class NSFont : NSObject {
12040 			void set() @selector("set"); // sets it into the current graphics context
12041 			void setInContext(NSGraphicsContext context) @selector("setInContext:");
12042 
12043 			static NSFont fontWithName(NSString fontName, CGFloat fontSize) @selector("fontWithName:size:");
12044 			// fontWithDescriptor too
12045 			// fontWithName and matrix too
12046 			static NSFont systemFontOfSize(CGFloat fontSize) @selector("systemFontOfSize:");
12047 			// among others
12048 
12049 			@property CGFloat pointSize() @selector("pointSize");
12050 			@property bool isFixedPitch() @selector("isFixedPitch");
12051 			// fontDescriptor
12052 			@property NSString displayName() @selector("displayName");
12053 
12054 			@property CGFloat ascender() @selector("ascender");
12055 			@property CGFloat descender() @selector("descender"); // note it is negative
12056 			@property CGFloat capHeight() @selector("capHeight");
12057 			@property CGFloat leading() @selector("leading");
12058 			@property CGFloat xHeight() @selector("xHeight");
12059 			// among many more
12060 		}
12061 
12062 		extern class NSColor : NSObject {
12063 			override static NSColor alloc() @selector("alloc");
12064 			static NSColor redColor() @selector("redColor");
12065 			static NSColor whiteColor() @selector("whiteColor");
12066 
12067 			CGColorRef CGColor() @selector("CGColor");
12068 		}
12069 
12070 		extern class CALayer : NSObject {
12071 			CGFloat borderWidth() @selector("borderWidth");
12072 			void borderWidth(CGFloat value) @selector("setBorderWidth:");
12073 
12074 			CGColorRef borderColor() @selector("borderColor");
12075 			void borderColor(CGColorRef) @selector("setBorderColor:");
12076 		}
12077 
12078 
12079 		version(OSX)
12080 		extern class NSViewController : NSObject {
12081 			NSView view() @selector("view");
12082 			void view(NSView view) @selector("setView:");
12083 		}
12084 
12085 		enum NSBackingStoreType : size_t {
12086 			retained = 0,
12087 			nonretained = 1,
12088 			buffered = 2
12089 		}
12090 
12091 		enum NSStringEncoding : NSUInteger {
12092 			NSASCIIStringEncoding = 1,		/* 0..127 only */
12093 			NSUTF8StringEncoding = 4,
12094 			NSUnicodeStringEncoding = 10,
12095 
12096 			NSUTF16StringEncoding = NSUnicodeStringEncoding,
12097 			NSUTF16BigEndianStringEncoding = 0x90000100,
12098 			NSUTF16LittleEndianStringEncoding = 0x94000100,
12099 			NSUTF32StringEncoding = 0x8c000100,
12100 			NSUTF32BigEndianStringEncoding = 0x98000100,
12101 			NSUTF32LittleEndianStringEncoding = 0x9c000100
12102 		}
12103 
12104 
12105 		struct CGColor;
12106 		alias CGColorRef = CGColor*;
12107 
12108 		// note on the watch os it is float, not double
12109 		alias CGFloat = double;
12110 
12111 		struct NSPoint {
12112 			CGFloat x;
12113 			CGFloat y;
12114 		}
12115 
12116 		struct NSSize {
12117 			CGFloat width;
12118 			CGFloat height;
12119 		}
12120 
12121 		struct NSRect {
12122 			NSPoint origin;
12123 			NSSize size;
12124 		}
12125 
12126 		alias NSPoint CGPoint;
12127 		alias NSSize CGSize;
12128 		alias NSRect CGRect;
12129 
12130 		pragma(inline, true) NSPoint NSMakePoint(CGFloat x, CGFloat y) {
12131 			NSPoint p;
12132 			p.x = x;
12133 			p.y = y;
12134 			return p;
12135 		}
12136 
12137 		pragma(inline, true) NSSize NSMakeSize(CGFloat w, CGFloat h) {
12138 			NSSize s;
12139 			s.width = w;
12140 			s.height = h;
12141 			return s;
12142 		}
12143 
12144 		pragma(inline, true) NSRect NSMakeRect(CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
12145 			NSRect r;
12146 			r.origin.x = x;
12147 			r.origin.y = y;
12148 			r.size.width = w;
12149 			r.size.height = h;
12150 			return r;
12151 		}
12152 
12153 
12154 	}
12155 
12156 	// helper raii refcount object
12157 	static if(UseCocoa)
12158 	struct MacString {
12159 		union {
12160 			// must be wrapped cuz of bug in dmd
12161 			// referencing an init symbol when it should
12162 			// just be null. but the union makes it work
12163 			NSString s;
12164 		}
12165 
12166 		// FIXME: if a string literal it would be kinda nice to use
12167 		// the other function. but meh
12168 
12169 		this(scope const char[] str) {
12170 			this.s = NSString.alloc.initWithBytes(
12171 				cast(const(ubyte)*) str.ptr,
12172 				str.length,
12173 				NSStringEncoding.NSUTF8StringEncoding
12174 			);
12175 		}
12176 
12177 		NSString borrow() {
12178 			return s;
12179 		}
12180 
12181 		this(this) {
12182 			if(s !is null)
12183 				s.retain();
12184 		}
12185 
12186 		~this() {
12187 			if(s !is null) {
12188 				s.release();
12189 				s = null;
12190 			}
12191 		}
12192 	}
12193 
12194 	extern(C) void NSLog(NSString, ...);
12195 	extern(C) SEL sel_registerName(const(char)* str);
12196 
12197 	version(OSX)
12198 	extern (Objective-C) __gshared NSApplication NSApp_;
12199 
12200 	version(OSX)
12201 	NSApplication NSApp() {
12202 		if(NSApp_ is null)
12203 			NSApp_ = NSApplication.shared_;
12204 		return NSApp_;
12205 	}
12206 
12207 	version(DigitalMars) {
12208 	// hacks to work around compiler bug
12209 	extern(C) __gshared void* _D4arsd4core17NSGraphicsContext7__ClassZ = null;
12210 	extern(C) __gshared void* _D4arsd4core6NSView7__ClassZ = null;
12211 	extern(C) __gshared void* _D4arsd4core8NSWindow7__ClassZ = null;
12212 	}
12213 
12214 
12215 
12216 	extern(C) { // grand central dispatch bindings
12217 
12218 		// /Library/Developer/CommandLineTools/SDKs/MacOSX13.1.sdk/usr/include/dispatch
12219 		// https://swiftlang.github.io/swift-corelibs-libdispatch/tutorial/
12220 		// https://man.freebsd.org/cgi/man.cgi?query=dispatch_main&sektion=3&apropos=0&manpath=macOS+14.3.1
12221 
12222 		struct dispatch_source_type_s {}
12223 		private __gshared immutable extern {
12224 			dispatch_source_type_s _dispatch_source_type_timer;
12225 			dispatch_source_type_s _dispatch_source_type_proc;
12226 			dispatch_source_type_s _dispatch_source_type_signal;
12227 			dispatch_source_type_s _dispatch_source_type_read;
12228 			dispatch_source_type_s _dispatch_source_type_write;
12229 			dispatch_source_type_s _dispatch_source_type_vnode;
12230 			// also memory pressure and some others
12231 		}
12232 
12233 		immutable DISPATCH_SOURCE_TYPE_TIMER = &_dispatch_source_type_timer;
12234 		immutable DISPATCH_SOURCE_TYPE_PROC = &_dispatch_source_type_proc;
12235 		immutable DISPATCH_SOURCE_TYPE_SIGNAL = &_dispatch_source_type_signal;
12236 		immutable DISPATCH_SOURCE_TYPE_READ = &_dispatch_source_type_read;
12237 		immutable DISPATCH_SOURCE_TYPE_WRITE = &_dispatch_source_type_write;
12238 		immutable DISPATCH_SOURCE_TYPE_VNODE = &_dispatch_source_type_vnode;
12239 		// also are some for internal data change things and a couple others
12240 
12241 		enum DISPATCH_PROC_EXIT = 0x80000000; // process exited
12242 		enum DISPATCH_PROC_FORK = 0x40000000; // it forked
12243 		enum DISPATCH_PROC_EXEC = 0x20000000; // it execed
12244 		enum DISPATCH_PROC_SIGNAL = 0x08000000; // it received a signal
12245 
12246 		enum DISPATCH_VNODE_DELETE = 0x1;
12247 		enum DISPATCH_VNODE_WRITE = 0x2;
12248 		enum DISPATCH_VNODE_EXTEND = 0x4;
12249 		enum DISPATCH_VNODE_ATTRIB = 0x8;
12250 		enum DISPATCH_VNODE_LINK = 0x10;
12251 		enum DISPATCH_VNODE_RENAME = 0x20;
12252 		enum DISPATCH_VNODE_REVOKE = 0x40;
12253 		enum DISPATCH_VNODE_FUNLOCK = 0x100;
12254 
12255 		private struct dispatch_source_s;
12256 		private struct dispatch_queue_s {}
12257 
12258 		alias dispatch_source_type_t = const(dispatch_source_type_s)*;
12259 
12260 		alias dispatch_source_t = dispatch_source_s*; // NSObject<OS_dispatch_source>
12261 		alias dispatch_queue_t = dispatch_queue_s*; // NSObject<OS_dispatch_queue>
12262 		alias dispatch_object_t = void*; // actually a "transparent union" of the dispatch_source_t, dispatch_queue_t, and others
12263 		alias dispatch_block_t = ObjCBlock!(void)*;
12264 		static if(typeof(null).sizeof == 8)
12265 			alias uintptr_t = ulong;
12266 		else
12267 			alias uintptr_t = uint;
12268 
12269 		dispatch_source_t dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, c_ulong mask, dispatch_queue_t queue);
12270 		void dispatch_source_set_event_handler(dispatch_source_t source, dispatch_block_t handler);
12271 		void dispatch_source_set_cancel_handler(dispatch_source_t source, dispatch_block_t handler);
12272 		void dispatch_source_cancel(dispatch_source_t source);
12273 
12274 		// DISPATCH_DECL_SUBCLASS(dispatch_queue_main, dispatch_queue_serial);
12275 		// dispatch_queue_t dispatch_get_main_queue();
12276 
12277 		extern __gshared dispatch_queue_s _dispatch_main_q;
12278 
12279 		extern(D) dispatch_queue_t dispatch_get_main_queue() {
12280 			return &_dispatch_main_q;
12281 		}
12282 
12283 		// FIXME: what is dispatch_time_t ???
12284 		// dispatch_time
12285 		// dispatch_walltime
12286 
12287 		// void dispatch_source_set_timer(dispatch_source_t source, dispatch_time_t start, ulong interval, ulong leeway);
12288 
12289 		void dispatch_retain(dispatch_object_t object);
12290 		void dispatch_release(dispatch_object_t object);
12291 
12292 		void dispatch_resume(dispatch_object_t object);
12293 		void dispatch_pause(dispatch_object_t object);
12294 
12295 		void* dispatch_get_context(dispatch_object_t object);
12296 		void dispatch_set_context(dispatch_object_t object, void* context);
12297 
12298 		// sends a function to the given queue
12299 		void dispatch_sync(dispatch_queue_t queue, scope dispatch_block_t block);
12300 		void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
12301 
12302 	} // grand central dispatch bindings
12303 
12304 }