mirror of https://github.com/adamdruppe/arsd.git
more cool stuff
This commit is contained in:
parent
f1d9574227
commit
e0626836e2
|
|
@ -29,7 +29,8 @@ Future release, likely May 2026 or later.
|
||||||
Planned changes:
|
Planned changes:
|
||||||
|
|
||||||
* Make arsd.http2 use arsd.core event loop by default (currently you still opt in with -version=use_arsd_core)
|
* Make arsd.http2 use arsd.core event loop by default (currently you still opt in with -version=use_arsd_core)
|
||||||
* Make arsd.cgi integrate with the arsd.core event loop
|
* Make arsd.terminal use the arsd.core event loop sometimes. This might not be a breaking change since it might be a new function and thus may come early.
|
||||||
|
* Make arsd.cgi integrate with the arsd.core event loop in some mode. Again, unlikely to be an actual break so may come early.
|
||||||
|
|
||||||
## 12.0
|
## 12.0
|
||||||
|
|
||||||
|
|
@ -38,7 +39,7 @@ Released: Planned for some time between January and May 2025
|
||||||
minigui's `defaultEventHandler_*` functions take more specific objects. So if you see errors like:
|
minigui's `defaultEventHandler_*` functions take more specific objects. So if you see errors like:
|
||||||
|
|
||||||
```
|
```
|
||||||
Error: function `void arsd.minigui.EditableTextWidget.defaultEventHandler_focusin(Event foe)` does not override any function, did you mean to override `void arsd.minigui.Widget.defaultEventHandler_focusin(arsd.minigui.FocusInEvent event)`?
|
Error: function `void arsd.minigui.EditableTextWidget.defaultEventHandler_focusin(Event event)` does not override any function, did you mean to override `void arsd.minigui.Widget.defaultEventHandler_focusin(arsd.minigui.FocusInEvent event)`?
|
||||||
```
|
```
|
||||||
|
|
||||||
Go to the file+line number from the error message and change `Event` to `FocusInEvent` (or whatever one it tells you in the "did you mean" part of the error) and recompile. No other changes should be necessary, however if you constructed your own `Event` object and dispatched it with the loosely typed `"focus"`, etc., strings, it may not trigger the default handlers anymore. To fix this, change any `new Event` to use the appropriate subclass, when available, like old `new Event("focus", widget);` changes to `new FocusEvent(widget)`. This only applies to ones that trigger default handlers present in `Widget` base class; your custom events still work the same way.
|
Go to the file+line number from the error message and change `Event` to `FocusInEvent` (or whatever one it tells you in the "did you mean" part of the error) and recompile. No other changes should be necessary, however if you constructed your own `Event` object and dispatched it with the loosely typed `"focus"`, etc., strings, it may not trigger the default handlers anymore. To fix this, change any `new Event` to use the appropriate subclass, when available, like old `new Event("focus", widget);` changes to `new FocusEvent(widget)`. This only applies to ones that trigger default handlers present in `Widget` base class; your custom events still work the same way.
|
||||||
|
|
@ -49,6 +50,8 @@ arsd.ini was added.
|
||||||
|
|
||||||
arsd.zip, arsd.xlsx, arsd.rtf, arsd.pptx, arsd.docx, all added.
|
arsd.zip, arsd.xlsx, arsd.rtf, arsd.pptx, arsd.docx, all added.
|
||||||
|
|
||||||
|
arsd.conv added.
|
||||||
|
|
||||||
arsd.script's language now requires () around the if and while conditions. Its parser is still so bad, but this let me fix some other bugs without making it even worse.
|
arsd.script's language now requires () around the if and while conditions. Its parser is still so bad, but this let me fix some other bugs without making it even worse.
|
||||||
|
|
||||||
arsd.simpledisplay now uses the arsd.core event loop by default.
|
arsd.simpledisplay now uses the arsd.core event loop by default.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,582 @@
|
||||||
|
/++
|
||||||
|
Module for helping to make command line interface programs.
|
||||||
|
|
||||||
|
|
||||||
|
You make an object with methods. Those methods take arguments and it reads them automatically for you. Or, you just make one function.
|
||||||
|
|
||||||
|
./yourprogram args...
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
./yourprogram class_method_name args....
|
||||||
|
|
||||||
|
Args go to:
|
||||||
|
bool: --name or --name=true|false
|
||||||
|
string/int/float/enum: --name=arg or --name arg
|
||||||
|
int[]: --name=arg,arg,arg or --name=arg --name=arg that you can repeat
|
||||||
|
string[] : remainder; the name is ignored, these are any args not already consumed by args
|
||||||
|
FilePath and FilePath[]: not yet supported
|
||||||
|
|
||||||
|
`--` always stops populating names and puts the remaining in the final string[] args param (if there is one)
|
||||||
|
`--help` always
|
||||||
|
|
||||||
|
Return values:
|
||||||
|
int is the return value to the cli
|
||||||
|
string is output, returns 0
|
||||||
|
other types are converted to string except for CliResult, which lets you specify output, error, and code in one struct.
|
||||||
|
Exceptions:
|
||||||
|
are printed with fairly minimal info to the stderr, cause program to return 1 unless it has a code attached
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added May 23, 2025
|
||||||
|
+/
|
||||||
|
module arsd.cli;
|
||||||
|
|
||||||
|
// stdin:
|
||||||
|
|
||||||
|
/++
|
||||||
|
You can pass a function to [runCli] and it will parse command line arguments
|
||||||
|
into its arguments, then turn its return value (if present) into a cli return.
|
||||||
|
+/
|
||||||
|
unittest {
|
||||||
|
static // exclude from docs
|
||||||
|
void func(int a, string[] otherArgs) {
|
||||||
|
// because we run the test below with args "--a 5"
|
||||||
|
assert(a == 5);
|
||||||
|
assert(otherArgs.length == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(string[] args) {
|
||||||
|
// make your main function forward to runCli!your_handler
|
||||||
|
return runCli!func(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(main(["unittest", "--a", "5"]) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
You can also pass a class to [runCli], and its public methods will be made
|
||||||
|
available as subcommands.
|
||||||
|
+/
|
||||||
|
unittest {
|
||||||
|
static // exclude from docs
|
||||||
|
class Thing {
|
||||||
|
void func(int a, string[] args) {
|
||||||
|
assert(a == 5);
|
||||||
|
assert(args.length == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// int return values are forwarded to `runCli`'s return value
|
||||||
|
int other(bool flag) {
|
||||||
|
return flag ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(string[] args) {
|
||||||
|
// make your main function forward to runCli!your_handler
|
||||||
|
return runCli!Thing(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(main(["unittest", "func", "--a", "5"]) == 0);
|
||||||
|
assert(main(["unittest", "other"]) == 0);
|
||||||
|
assert(main(["unittest", "other", "--flag"]) == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
import arsd.core;
|
||||||
|
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
int runCli(alias handler)(string[] args) {
|
||||||
|
CliHandler thing;
|
||||||
|
|
||||||
|
static if(is(handler == class)) {
|
||||||
|
CliHandler[] allOptions;
|
||||||
|
|
||||||
|
scope auto instance = new handler();
|
||||||
|
foreach(memberName; __traits(derivedMembers, handler)) {
|
||||||
|
static if(memberName != "__ctor" && memberName != "__dtor") {
|
||||||
|
alias member = __traits(getMember, handler, memberName);
|
||||||
|
static if(__traits(getProtection, member) == "public") {
|
||||||
|
static if(is(typeof(member) == return)) {
|
||||||
|
auto ourthing = createCliHandler!member();
|
||||||
|
if(args.length > 1 && ourthing.uda.name == args[1]) {
|
||||||
|
thing = ourthing;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
allOptions ~= ourthing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(args.length && args[1] == "--help") {
|
||||||
|
foreach(option; allOptions)
|
||||||
|
writeln(option.printHelp());
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(args.length)
|
||||||
|
args = args[1 .. $]; // cut off the original args(0) as irrelevant now, the command is the new args[0]
|
||||||
|
} else {
|
||||||
|
auto instance = null;
|
||||||
|
thing = createCliHandler!handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!thing.uda.unprocessed && args.length > 1 && args[1] == "--help") {
|
||||||
|
writeln(thing.printHelp());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(thing.handler is null) {
|
||||||
|
throw new CliArgumentException("subcommand", "no handler found");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ret = thing.handler(thing, instance, args);
|
||||||
|
if(ret.output.length)
|
||||||
|
writeln(ret.output);
|
||||||
|
if(ret.error.length)
|
||||||
|
writelnStderr(ret.error);
|
||||||
|
return ret.returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
class CliArgumentException : object.Exception {
|
||||||
|
this(string argument, string message) {
|
||||||
|
super(argument ~ ": " ~ message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
If your function returns `CliResult`, you can return a value and some output in one object.
|
||||||
|
|
||||||
|
Note that output and error are written to stdout and stderr, in addition to whatever the function
|
||||||
|
did inside. It does NOT represent captured stuff, it is just a function return value.
|
||||||
|
+/
|
||||||
|
struct CliResult {
|
||||||
|
int returnValue;
|
||||||
|
string output;
|
||||||
|
string error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Can be attached as a UDA to override defaults
|
||||||
|
+/
|
||||||
|
struct Cli {
|
||||||
|
string name;
|
||||||
|
|
||||||
|
string summary;
|
||||||
|
string help;
|
||||||
|
|
||||||
|
// only valid on function - passes the original args without processing them at all, not even --help
|
||||||
|
bool unprocessed; // FIXME mostly not implemented
|
||||||
|
// only valid on function - instead of erroring on unknown arg, just pass them unmodified to the catch-all array
|
||||||
|
bool passthroughUnrecognizedArguments; // FIXME not implemented
|
||||||
|
|
||||||
|
|
||||||
|
// only valid on arguments
|
||||||
|
dchar shortName; // bool things can be combined and if it is int it can take one like -O2. maybe.
|
||||||
|
int required = 2;
|
||||||
|
int arg0 = 2;
|
||||||
|
int consumesRemainder = 2;
|
||||||
|
int holdsAllArgs = 2; // FIXME: not implemented
|
||||||
|
string[] options; // FIXME if it is not one of the options and there are options, should it error?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
version(sample)
|
||||||
|
void handler(bool sweetness, @Cli(arg0: true) string programName, float f, @Cli(required: true) int a, @Cli(name: "opend-to-build") string[] magic, int[] foo, string[] remainder) {
|
||||||
|
import arsd.core;
|
||||||
|
|
||||||
|
if(a == 4)
|
||||||
|
throw ArsdException!"lol"(4, 6);
|
||||||
|
|
||||||
|
mixin(dumpParams);
|
||||||
|
debug dump(__traits(parameters));
|
||||||
|
debug dump(i"$programName");
|
||||||
|
|
||||||
|
static struct Test {
|
||||||
|
int a;
|
||||||
|
string b;
|
||||||
|
float c;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug dump(Test(a: 5, b: "omg", c: 7.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
version(sample)
|
||||||
|
int main(string[] args) {
|
||||||
|
/+
|
||||||
|
import arsd.core;
|
||||||
|
auto e = extractCliArgs(args, false, ["a":true]);
|
||||||
|
foreach(a; e)
|
||||||
|
writeln(a.name, a.values);
|
||||||
|
return 0;
|
||||||
|
+/
|
||||||
|
|
||||||
|
return runCli!handler(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SupportedCliTypes {
|
||||||
|
String,
|
||||||
|
Int,
|
||||||
|
Float,
|
||||||
|
Bool,
|
||||||
|
IntArray,
|
||||||
|
StringArray
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CliArg {
|
||||||
|
Cli uda;
|
||||||
|
string argumentName;
|
||||||
|
string ddoc;
|
||||||
|
SupportedCliTypes type;
|
||||||
|
//string default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CliHandler {
|
||||||
|
CliResult function(CliHandler info, Object _this, string[] args) handler;
|
||||||
|
Cli uda;
|
||||||
|
CliArg[] args;
|
||||||
|
|
||||||
|
string methodName;
|
||||||
|
string ddoc;
|
||||||
|
|
||||||
|
string printHelp() {
|
||||||
|
string help = uda.name;
|
||||||
|
if(help.length)
|
||||||
|
help ~= ": ";
|
||||||
|
help ~= uda.help;
|
||||||
|
foreach(arg; args) {
|
||||||
|
if(!arg.uda.required)
|
||||||
|
help ~= "[";
|
||||||
|
if(arg.uda.consumesRemainder)
|
||||||
|
help ~= "args...";
|
||||||
|
else if(arg.type == SupportedCliTypes.Bool)
|
||||||
|
help ~= "--" ~ arg.uda.name;
|
||||||
|
else
|
||||||
|
help ~= "--" ~ arg.uda.name ~ "=" ~ enumNameForValue(arg.type);
|
||||||
|
if(!arg.uda.required)
|
||||||
|
help ~= "]";
|
||||||
|
help ~= " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: print the help details for the args
|
||||||
|
|
||||||
|
return help;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private template CliTypeForD(T) {
|
||||||
|
static if(is(T == enum))
|
||||||
|
enum CliTypeForD = SupportedCliTypes.String;
|
||||||
|
else static if(is(T == string))
|
||||||
|
enum CliTypeForD = SupportedCliTypes.String;
|
||||||
|
else static if(is(T == bool))
|
||||||
|
enum CliTypeForD = SupportedCliTypes.Bool;
|
||||||
|
else static if(is(T : long))
|
||||||
|
enum CliTypeForD = SupportedCliTypes.Int;
|
||||||
|
else static if(is(T : double))
|
||||||
|
enum CliTypeForD = SupportedCliTypes.Float;
|
||||||
|
else static if(is(T : int[]))
|
||||||
|
enum CliTypeForD = SupportedCliTypes.IntArray;
|
||||||
|
else static if(is(T : string[]))
|
||||||
|
enum CliTypeForD = SupportedCliTypes.StringArray;
|
||||||
|
else
|
||||||
|
static assert(0, "Unsupported type for CLI: " ~ T.stringof);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CliHandler createCliHandler(alias handler)() {
|
||||||
|
CliHandler ret;
|
||||||
|
|
||||||
|
ret.methodName = __traits(identifier, handler);
|
||||||
|
version(D_OpenD)
|
||||||
|
ret.ddoc = __traits(docComment, handler);
|
||||||
|
|
||||||
|
foreach(uda; __traits(getAttributes, handler))
|
||||||
|
static if(is(typeof(uda) == Cli))
|
||||||
|
ret.uda = uda;
|
||||||
|
|
||||||
|
if(ret.uda.name is null)
|
||||||
|
ret.uda.name = ret.methodName;
|
||||||
|
if(ret.uda.help is null)
|
||||||
|
ret.uda.help = ret.ddoc;
|
||||||
|
if(ret.uda.summary is null)
|
||||||
|
ret.uda.summary = ret.uda.help; // FIXME: abbreviate
|
||||||
|
|
||||||
|
static if(is(typeof(handler) Params == __parameters))
|
||||||
|
foreach(idx, param; Params) {
|
||||||
|
CliArg arg;
|
||||||
|
|
||||||
|
arg.argumentName = __traits(identifier, Params[idx .. idx + 1]);
|
||||||
|
// version(D_OpenD) arg.ddoc = __traits(docComment, Params[idx .. idx + 1]);
|
||||||
|
|
||||||
|
arg.type = CliTypeForD!param;
|
||||||
|
|
||||||
|
foreach(uda; __traits(getAttributes, Params[idx .. idx + 1]))
|
||||||
|
static if(is(typeof(uda) == Cli)) {
|
||||||
|
arg.uda = uda;
|
||||||
|
// import std.stdio; writeln(cast(int) uda.arg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// if not specified by user, replace with actual defaults
|
||||||
|
if(arg.uda.consumesRemainder == 2) {
|
||||||
|
if(idx + 1 == Params.length && is(param == string[]))
|
||||||
|
arg.uda.consumesRemainder = true;
|
||||||
|
else
|
||||||
|
arg.uda.consumesRemainder = false;
|
||||||
|
} else {
|
||||||
|
assert(0, "do not set consumesRemainder explicitly at least not at this time");
|
||||||
|
}
|
||||||
|
if(arg.uda.arg0 == 2)
|
||||||
|
arg.uda.arg0 = false;
|
||||||
|
if(arg.uda.required == 2)
|
||||||
|
arg.uda.required = false;
|
||||||
|
if(arg.uda.holdsAllArgs == 2)
|
||||||
|
arg.uda.holdsAllArgs = false;
|
||||||
|
static if(is(param == enum))
|
||||||
|
if(arg.uda.options is null)
|
||||||
|
arg.uda.options = [__traits(allMembers, param)];
|
||||||
|
|
||||||
|
if(arg.uda.name is null)
|
||||||
|
arg.uda.name = arg.argumentName;
|
||||||
|
|
||||||
|
ret.args ~= arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.handler = &cliForwarder!handler;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ExtractedCliArgs {
|
||||||
|
string name;
|
||||||
|
string[] values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtractedCliArgs[] extractCliArgs(string[] args, bool needsCommandName, bool[string] namesThatTakeSeparateArguments) {
|
||||||
|
// FIXME: if needsCommandName, args[1] should be that
|
||||||
|
ExtractedCliArgs[] ret;
|
||||||
|
if(args.length == 0)
|
||||||
|
return [ExtractedCliArgs(), ExtractedCliArgs()];
|
||||||
|
|
||||||
|
ExtractedCliArgs remainder;
|
||||||
|
|
||||||
|
ret ~= ExtractedCliArgs(null, [args[0]]); // arg0 is a bit special, always the first one
|
||||||
|
args = args[1 .. $];
|
||||||
|
|
||||||
|
ref ExtractedCliArgs byName(string name) {
|
||||||
|
// FIXME: could actually do a map to index thing if i had to
|
||||||
|
foreach(ref r; ret)
|
||||||
|
if(r.name == name)
|
||||||
|
return r;
|
||||||
|
ret ~= ExtractedCliArgs(name);
|
||||||
|
return ret[$-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
string nextArgName = null;
|
||||||
|
|
||||||
|
void appendPossibleEmptyArg() {
|
||||||
|
if(nextArgName is null)
|
||||||
|
return;
|
||||||
|
byName(nextArgName).values ~= null;
|
||||||
|
nextArgName = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(idx, arg; args) {
|
||||||
|
if(arg == "--") {
|
||||||
|
remainder.values ~= args[idx + 1 .. $];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(arg[0] == '-') {
|
||||||
|
// short name or short nameINT_VALUE
|
||||||
|
// -longname or -longname=VALUE. if -longname, next arg is its value unless next arg starts with -.
|
||||||
|
|
||||||
|
if(arg.length == 1) {
|
||||||
|
// plain - often represents stdin or whatever, treat it as a normal filename arg
|
||||||
|
remainder.values ~= arg;
|
||||||
|
} else {
|
||||||
|
appendPossibleEmptyArg();
|
||||||
|
|
||||||
|
string value;
|
||||||
|
if(arg[1] == '-') {
|
||||||
|
// long name...
|
||||||
|
import arsd.string;
|
||||||
|
auto equal = arg.indexOf("=");
|
||||||
|
if(equal != -1) {
|
||||||
|
nextArgName = arg[2 .. equal];
|
||||||
|
value = arg[equal + 1 .. $];
|
||||||
|
} else {
|
||||||
|
nextArgName = arg[2 .. $];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// short name
|
||||||
|
nextArgName = arg[1 .. $]; // FIXME what if there's bundled? or an arg?
|
||||||
|
}
|
||||||
|
byName(nextArgName);
|
||||||
|
if(value !is null) {
|
||||||
|
byName(nextArgName).values ~= value;
|
||||||
|
nextArgName = null;
|
||||||
|
} else if(!namesThatTakeSeparateArguments.get(nextArgName, false)) {
|
||||||
|
byName(nextArgName).values ~= null; // just so you can see how many times it appeared
|
||||||
|
nextArgName = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(nextArgName !is null) {
|
||||||
|
byName(nextArgName).values ~= arg;
|
||||||
|
|
||||||
|
nextArgName = null;
|
||||||
|
} else {
|
||||||
|
remainder.values ~= arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendPossibleEmptyArg();
|
||||||
|
|
||||||
|
ret ~= remainder; // remainder also a bit special, always the last one
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: extractPrefix for stuff like --opend-to-build and --DRT- stuff
|
||||||
|
|
||||||
|
private T extractCliArgsT(T)(CliArg info, ExtractedCliArgs[] args) {
|
||||||
|
try {
|
||||||
|
import arsd.conv;
|
||||||
|
if(info.uda.arg0) {
|
||||||
|
static if(is(T == string)) {
|
||||||
|
return args[0].values[0];
|
||||||
|
} else {
|
||||||
|
assert(0, "arg0 consumers must be type string");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(info.uda.consumesRemainder)
|
||||||
|
static if(is(T == string[])) {
|
||||||
|
return args[$-1].values;
|
||||||
|
} else {
|
||||||
|
assert(0, "remainder consumers must be type string[]");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(arg; args)
|
||||||
|
if(arg.name == info.uda.name) {
|
||||||
|
static if(is(T == string[]))
|
||||||
|
return arg.values;
|
||||||
|
else static if(is(T == int[])) {
|
||||||
|
int[] ret;
|
||||||
|
ret.length = arg.values.length;
|
||||||
|
foreach(i, a; arg.values)
|
||||||
|
ret[i] = to!int(a);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
} else static if(is(T == bool)) {
|
||||||
|
// if the argument is present, that means it is set unless the value false was explicitly given
|
||||||
|
if(arg.values.length)
|
||||||
|
return arg.values[$-1] != "false";
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if(arg.values.length == 1)
|
||||||
|
return to!T(arg.values[$-1]);
|
||||||
|
else
|
||||||
|
throw ArsdException!"wrong number of args"(arg.values.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return T.init;
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new CliArgumentException(info.uda.name, e.toString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CliResult cliForwarder(alias handler)(CliHandler info, Object this_, string[] args) {
|
||||||
|
try {
|
||||||
|
static if(is(typeof(handler) Params == __parameters))
|
||||||
|
Params params;
|
||||||
|
|
||||||
|
assert(Params.length == info.args.length);
|
||||||
|
|
||||||
|
bool[string] map;
|
||||||
|
foreach(a; info.args)
|
||||||
|
if(a.type != SupportedCliTypes.Bool)
|
||||||
|
map[a.uda.name] = true;
|
||||||
|
auto eargs = extractCliArgs(args, false, map);
|
||||||
|
|
||||||
|
/+
|
||||||
|
import arsd.core;
|
||||||
|
foreach(a; eargs)
|
||||||
|
writeln(a.name, a.values);
|
||||||
|
+/
|
||||||
|
|
||||||
|
foreach(a; eargs[1 .. $-1]) {
|
||||||
|
bool found;
|
||||||
|
foreach(a2; info.args)
|
||||||
|
if(a.name == a2.uda.name) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(!found)
|
||||||
|
throw new CliArgumentException(a.name, "Invalid arg");
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: look for missing required argument
|
||||||
|
foreach(a; info.args) {
|
||||||
|
if(a.uda.required) {
|
||||||
|
bool found = false;
|
||||||
|
foreach(a2; eargs[1 .. $-1]) {
|
||||||
|
if(a2.name == a.uda.name) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!found)
|
||||||
|
throw new CliArgumentException(a.uda.name, "Missing required arg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(idx, ref param; params) {
|
||||||
|
param = extractCliArgsT!(typeof(param))(info.args[idx], eargs);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto callit() {
|
||||||
|
static if(is(__traits(parent, handler) Parent == class)) {
|
||||||
|
auto instance = cast(Parent) this_;
|
||||||
|
assert(instance !is null);
|
||||||
|
return __traits(child, instance, handler)(params);
|
||||||
|
} else {
|
||||||
|
return handler(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static if(is(typeof(handler) Return == return)) {
|
||||||
|
static if(is(Return == void)) {
|
||||||
|
callit();
|
||||||
|
return CliResult(0);
|
||||||
|
} else static if(is(Return == int)) {
|
||||||
|
return CliResult(callit());
|
||||||
|
} else static if(is(Return == string)) {
|
||||||
|
return CliResult(0, callit());
|
||||||
|
} else static assert(0, "Invalid return type on handler: " ~ Return.stringof);
|
||||||
|
} else static assert(0, "bad handler");
|
||||||
|
} catch(CliArgumentException e) {
|
||||||
|
auto str = e.msg;
|
||||||
|
auto idx = str.indexOf("------");
|
||||||
|
if(idx != -1)
|
||||||
|
str = str[0 .. idx];
|
||||||
|
str = str.stripInternal();
|
||||||
|
return CliResult(1, null, str);
|
||||||
|
} catch(Throwable t) {
|
||||||
|
auto str = t.toString;
|
||||||
|
auto idx = str.indexOf("------");
|
||||||
|
if(idx != -1)
|
||||||
|
str = str[0 .. idx];
|
||||||
|
str = str.stripInternal();
|
||||||
|
return CliResult(1, null, str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,351 @@
|
||||||
|
/++
|
||||||
|
A simplified version of `std.conv` with better error messages and faster compiles for supported types.
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added May 22, 2025
|
||||||
|
+/
|
||||||
|
module arsd.conv;
|
||||||
|
|
||||||
|
static import arsd.core;
|
||||||
|
|
||||||
|
// FIXME: thousands separator for int to string (and float to string)
|
||||||
|
// FIXME: intToStringArgs
|
||||||
|
// FIXME: floatToStringArgs
|
||||||
|
|
||||||
|
/++
|
||||||
|
Converts a string into the other given type. Throws on failure.
|
||||||
|
+/
|
||||||
|
T to(T)(scope const(char)[] str) {
|
||||||
|
static if(is(T : long)) {
|
||||||
|
// FIXME: unsigned? overflowing? radix? keep reading or stop on invalid char?
|
||||||
|
StringToIntArgs args;
|
||||||
|
args.unsigned = __traits(isUnsigned, T);
|
||||||
|
long v = stringToInt(str, args);
|
||||||
|
T ret = cast(T) v;
|
||||||
|
if(ret != v)
|
||||||
|
throw new StringToIntConvException("overflow", 0, str.idup, 0);
|
||||||
|
return ret;
|
||||||
|
} else static if(is(T : double)) {
|
||||||
|
import core.stdc.stdlib;
|
||||||
|
import core.stdc.errno;
|
||||||
|
arsd.core.CharzBuffer z = str;
|
||||||
|
char* end;
|
||||||
|
errno = 0;
|
||||||
|
double res = strtod(z.ptr, &end);
|
||||||
|
if(end !is (z.ptr + z.length) || errno) {
|
||||||
|
string msg = errno == ERANGE ? "Over/underflow" : "Invalid input";
|
||||||
|
throw new StringToIntConvException(msg, 10, str.idup, end - z.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
static assert(0, "Unsupported type: " ~ T.stringof);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Converts any given value to a string. The format of the string is unspecified; it is meant for a human reader and might be overridden by types.
|
||||||
|
+/
|
||||||
|
string to(T:string, From)(From value) {
|
||||||
|
static if(is(From == enum))
|
||||||
|
return arsd.core.enumNameForValue(value);
|
||||||
|
else
|
||||||
|
return arsd.core.toStringInternal(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/+
|
||||||
|
T to(T, F)(F value) if(!is(F : const(char)[])) {
|
||||||
|
// if the language allows implicit conversion, let it do its thing
|
||||||
|
static if(is(T : F)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
// integral type conversions do checked things
|
||||||
|
static if(is(T : long) && is(F : long)) {
|
||||||
|
return checkedConversion!T(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
// array to array conversion: try to convert the individual elements, allocating a new return value.
|
||||||
|
static if(is(T : TE[], TE) && is(F : FE[], FE)) {
|
||||||
|
F ret = new F(value.length);
|
||||||
|
foreach(i, e; value)
|
||||||
|
ret[i] = to!TE(e);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
static assert(0, "Unsupported conversion types");
|
||||||
|
}
|
||||||
|
+/
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
assert(to!int("5") == 5);
|
||||||
|
assert(to!int("35") == 35);
|
||||||
|
assert(to!string(35) == "35");
|
||||||
|
assert(to!int("0xA35d") == 0xA35d);
|
||||||
|
assert(to!int("0b11001001") == 0b11001001);
|
||||||
|
assert(to!int("0o777") == 511 /*0o777*/);
|
||||||
|
|
||||||
|
assert(to!ubyte("255") == 255);
|
||||||
|
assert(to!ulong("18446744073709551615") == ulong.max);
|
||||||
|
|
||||||
|
void expectedToThrow(T...)(lazy T items) {
|
||||||
|
int count;
|
||||||
|
string messages;
|
||||||
|
static foreach(idx, item; items) {
|
||||||
|
try {
|
||||||
|
auto result = item;
|
||||||
|
if(messages.length)
|
||||||
|
messages ~= ",";
|
||||||
|
messages ~= idx.stringof[0..$-2];
|
||||||
|
} catch(StringToIntConvException e) {
|
||||||
|
// passed the test; it was supposed to throw.
|
||||||
|
// arsd.core.writeln(e);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(count == T.length, "Arg(s) " ~ messages ~ " did not throw");
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedToThrow(
|
||||||
|
to!uint("-44"), // negative number to unsigned reuslt
|
||||||
|
to!int("add"), // invalid base 10 chars
|
||||||
|
to!byte("129"), // wrapped to negative
|
||||||
|
to!int("0p4a0"), // invalid radix prefix
|
||||||
|
to!int("5000000000"), // doesn't fit in int
|
||||||
|
to!ulong("6000000000000000000900"), // overflow when reading into the ulong buffer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
class ValueOutOfRangeException : arsd.core.ArsdExceptionBase {
|
||||||
|
this(string type, long userSuppliedValue, long minimumAcceptableValue, long maximumAcceptableValue, string file = __FILE__, size_t line = __LINE__) {
|
||||||
|
this.type = type;
|
||||||
|
this.userSuppliedValue = userSuppliedValue;
|
||||||
|
this.minimumAcceptableValue = minimumAcceptableValue;
|
||||||
|
this.maximumAcceptableValue = maximumAcceptableValue;
|
||||||
|
super("Value was out of range", file, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
string type;
|
||||||
|
long userSuppliedValue;
|
||||||
|
long minimumAcceptableValue;
|
||||||
|
long maximumAcceptableValue;
|
||||||
|
|
||||||
|
override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const {
|
||||||
|
sink("type", type);
|
||||||
|
sink("userSuppliedValue", arsd.core.toStringInternal(userSuppliedValue));
|
||||||
|
sink("minimumAcceptableValue", arsd.core.toStringInternal(minimumAcceptableValue));
|
||||||
|
sink("maximumAcceptableValue", arsd.core.toStringInternal(maximumAcceptableValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
class StringToIntConvException : arsd.core.ArsdExceptionBase /*InvalidDataException*/ {
|
||||||
|
this(string msg, int radix, string userInput, size_t offset, string file = __FILE__, size_t line = __LINE__) {
|
||||||
|
this.radix = radix;
|
||||||
|
this.userInput = userInput;
|
||||||
|
this.offset = offset;
|
||||||
|
|
||||||
|
super(msg, file, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const {
|
||||||
|
sink("radix", arsd.core.toStringInternal(radix));
|
||||||
|
sink("userInput", arsd.core.toStringInternal(userInput));
|
||||||
|
if(offset < userInput.length)
|
||||||
|
sink("offset", arsd.core.toStringInternal(offset) ~ " ('" ~ userInput[offset] ~ "')");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
int radix;
|
||||||
|
///
|
||||||
|
string userInput;
|
||||||
|
///
|
||||||
|
size_t offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
if radix is 0, guess from 0o, 0x, 0b prefixes.
|
||||||
|
+/
|
||||||
|
long stringToInt(scope const(char)[] str, StringToIntArgs args = StringToIntArgs.init) {
|
||||||
|
long accumulator;
|
||||||
|
|
||||||
|
auto original = str;
|
||||||
|
|
||||||
|
Exception exception(string msg, size_t loopOffset = 0, string file = __FILE__, size_t line = __LINE__) {
|
||||||
|
return new StringToIntConvException(msg, args.radix, original.dup, loopOffset + str.ptr - original.ptr, file, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(str.length == 0)
|
||||||
|
throw exception("empty string");
|
||||||
|
|
||||||
|
bool isNegative;
|
||||||
|
if(str[0] == '-') {
|
||||||
|
if(args.unsigned)
|
||||||
|
throw exception("negative number given, but unsigned result desired");
|
||||||
|
|
||||||
|
isNegative = true;
|
||||||
|
str = str[1 .. $];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(str.length == 0)
|
||||||
|
throw exception("just a dash");
|
||||||
|
|
||||||
|
if(str[0] == '0') {
|
||||||
|
if(str.length > 1 && (str[1] == 'b' || str[1] == 'x' || str[1] == 'o')) {
|
||||||
|
if(args.radix != 0) {
|
||||||
|
throw exception("string had specified base, but the radix arg was already supplied");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(str[1]) {
|
||||||
|
case 'b':
|
||||||
|
args.radix = 2;
|
||||||
|
break;
|
||||||
|
case 'o':
|
||||||
|
args.radix = 8;
|
||||||
|
break;
|
||||||
|
case 'x':
|
||||||
|
args.radix = 16;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assert(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
str = str[2 .. $];
|
||||||
|
|
||||||
|
if(str.length == 0)
|
||||||
|
throw exception("just a prefix");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(args.radix == 0)
|
||||||
|
args.radix = 10;
|
||||||
|
|
||||||
|
foreach(idx, char ch; str) {
|
||||||
|
|
||||||
|
if(ch && ch == args.ignoredSeparator)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto before = accumulator;
|
||||||
|
|
||||||
|
accumulator *= args.radix;
|
||||||
|
|
||||||
|
int value = -1;
|
||||||
|
if(ch >= '0' && ch <= '9') {
|
||||||
|
value = ch - '0';
|
||||||
|
} else {
|
||||||
|
ch |= 32;
|
||||||
|
if(ch >= 'a' && ch <= 'z')
|
||||||
|
value = ch - 'a' + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(value < 0)
|
||||||
|
throw exception("invalid char", idx);
|
||||||
|
if(value >= args.radix)
|
||||||
|
throw exception("invalid char for given radix", idx);
|
||||||
|
|
||||||
|
accumulator += value;
|
||||||
|
if(args.unsigned) {
|
||||||
|
auto b = cast(ulong) before;
|
||||||
|
auto a = cast(ulong) accumulator;
|
||||||
|
if(a < b)
|
||||||
|
throw exception("value too big to fit in unsigned buffer", idx);
|
||||||
|
} else {
|
||||||
|
if(accumulator < before && !args.unsigned)
|
||||||
|
throw exception("value too big to fit in signed buffer", idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isNegative)
|
||||||
|
accumulator = -accumulator;
|
||||||
|
|
||||||
|
return accumulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ditto
|
||||||
|
struct StringToIntArgs {
|
||||||
|
int radix;
|
||||||
|
bool unsigned;
|
||||||
|
char ignoredSeparator = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Converts two integer types, returning the min/max of the desired type if the given value is out of range for it.
|
||||||
|
+/
|
||||||
|
T saturatingConversion(T)(long value) {
|
||||||
|
static assert(is(T : long), "Only works on integer types");
|
||||||
|
|
||||||
|
static if(is(T == ulong)) // the special case to try to handle the full range there
|
||||||
|
ulong mv = cast(ulong) value;
|
||||||
|
else
|
||||||
|
long mv = value;
|
||||||
|
|
||||||
|
if(mv > T.max)
|
||||||
|
return T.max;
|
||||||
|
else if(value < T.min)
|
||||||
|
return T.min;
|
||||||
|
else
|
||||||
|
return cast(T) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
assert(saturatingConversion!ubyte(256) == 255);
|
||||||
|
assert(saturatingConversion!byte(256) == 127);
|
||||||
|
assert(saturatingConversion!byte(-256) == -128);
|
||||||
|
|
||||||
|
assert(saturatingConversion!ulong(0) == 0);
|
||||||
|
assert(saturatingConversion!long(-5) == -5);
|
||||||
|
|
||||||
|
assert(saturatingConversion!uint(-5) == 0);
|
||||||
|
|
||||||
|
// assert(saturatingConversion!ulong(-5) == 0); // it can't catch this since the -5 is indistinguishable from the large ulong value here
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Truncates off bits that won't fit; equivalent to a built-in cast operation (you can just use a cast instead if you want).
|
||||||
|
+/
|
||||||
|
T truncatingConversion(T)(long value) {
|
||||||
|
static assert(is(T : long), "Only works on integer types");
|
||||||
|
|
||||||
|
return cast(T) value;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Converts two integer types, throwing an exception if the given value is out of range for it.
|
||||||
|
+/
|
||||||
|
T checkedConversion(T)(long value, long minimumAcceptableValue = T.min, long maximumAcceptableValue = T.max) {
|
||||||
|
static assert(is(T : long), "Only works on integer types");
|
||||||
|
|
||||||
|
if(value > maximumAcceptableValue)
|
||||||
|
throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue);
|
||||||
|
else if(value < minimumAcceptableValue)
|
||||||
|
throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue);
|
||||||
|
else
|
||||||
|
return cast(T) value;
|
||||||
|
}
|
||||||
|
/// ditto
|
||||||
|
T checkedConversion(T:ulong)(ulong value, ulong minimumAcceptableValue = T.min, ulong maximumAcceptableValue = T.max) {
|
||||||
|
if(value > maximumAcceptableValue)
|
||||||
|
throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue);
|
||||||
|
else if(value < minimumAcceptableValue)
|
||||||
|
throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue);
|
||||||
|
else
|
||||||
|
return cast(T) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
try {
|
||||||
|
assert(checkedConversion!byte(155));
|
||||||
|
assert(0);
|
||||||
|
} catch(ValueOutOfRangeException e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
300
core.d
300
core.d
|
|
@ -1034,7 +1034,7 @@ struct LimitedVariant {
|
||||||
auto d = getDouble();
|
auto d = getDouble();
|
||||||
|
|
||||||
import core.stdc.stdio;
|
import core.stdc.stdio;
|
||||||
char[128] buffer;
|
char[64] buffer;
|
||||||
auto count = snprintf(buffer.ptr, buffer.length, "%.17lf", d);
|
auto count = snprintf(buffer.ptr, buffer.length, "%.17lf", d);
|
||||||
return buffer[0 .. count].idup;
|
return buffer[0 .. count].idup;
|
||||||
case invalid:
|
case invalid:
|
||||||
|
|
@ -1378,6 +1378,7 @@ struct CharzBuffer {
|
||||||
|
|
||||||
buffer[0 .. data.length] = data[];
|
buffer[0 .. data.length] = data[];
|
||||||
buffer[data.length] = 0;
|
buffer[data.length] = 0;
|
||||||
|
buffer = buffer[0 .. data.length + 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1558,19 +1559,31 @@ char[] intToString(long value, char[] buffer, IntToStringArgs args = IntToString
|
||||||
|
|
||||||
int start = pos;
|
int start = pos;
|
||||||
int digitCount;
|
int digitCount;
|
||||||
|
int groupCount;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
auto remainder = value % radix;
|
auto remainder = value % radix;
|
||||||
value = value / radix;
|
value = value / radix;
|
||||||
|
|
||||||
|
if(groupSize && groupCount == groupSize) {
|
||||||
|
buffer[pos++] = args.separator;
|
||||||
|
groupCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
buffer[pos++] = cast(char) (remainder < 10 ? (remainder + '0') : (remainder - 10 + args.ten));
|
buffer[pos++] = cast(char) (remainder < 10 ? (remainder + '0') : (remainder - 10 + args.ten));
|
||||||
|
groupCount++;
|
||||||
digitCount++;
|
digitCount++;
|
||||||
} while(value);
|
} while(value);
|
||||||
|
|
||||||
if(digitsPad > 0) {
|
if(digitsPad > 0) {
|
||||||
while(digitCount < digitsPad) {
|
while(digitCount < digitsPad) {
|
||||||
|
if(groupSize && groupCount == groupSize) {
|
||||||
|
buffer[pos++] = args.separator;
|
||||||
|
groupCount = 0;
|
||||||
|
}
|
||||||
buffer[pos++] = args.padWith;
|
buffer[pos++] = args.padWith;
|
||||||
digitCount++;
|
digitCount++;
|
||||||
|
groupCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1621,6 +1634,105 @@ struct IntToStringArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FloatToStringArgs {
|
||||||
|
private {
|
||||||
|
// whole number component
|
||||||
|
ubyte padTo;
|
||||||
|
char padWith;
|
||||||
|
ubyte groupSize;
|
||||||
|
char separator;
|
||||||
|
|
||||||
|
// for the fractional component
|
||||||
|
ubyte minimumPrecision = 0; // will always show at least this many digits after the decimal (if it is 0 there may be no decimal)
|
||||||
|
ubyte maximumPrecision = 32; // will round to this many after the decimal
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatToStringArgs withPadding(int padTo, char padWith = '0') {
|
||||||
|
FloatToStringArgs args = this;
|
||||||
|
args.padTo = cast(ubyte) padTo;
|
||||||
|
args.padWith = padWith;
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatToStringArgs withGroupSeparator(int groupSize, char separator = '_') {
|
||||||
|
FloatToStringArgs args = this;
|
||||||
|
args.groupSize = cast(ubyte) groupSize;
|
||||||
|
args.separator = separator;
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatToStringArgs withPrecision(int minDigits, int maxDigits = 0) {
|
||||||
|
FloatToStringArgs args = this;
|
||||||
|
args.minimumPrecision = cast(ubyte) minDigits;
|
||||||
|
if(maxDigits < minDigits)
|
||||||
|
maxDigits = minDigits;
|
||||||
|
args.maximumPrecision = cast(ubyte) maxDigits;
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatToStringArgs withScientificNotation(bool enabled) {
|
||||||
|
FloatToStringArgs args = this;
|
||||||
|
args.useScientificNotation = enabled;
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char[] floatToString(double value, char[] buffer, FloatToStringArgs args = FloatToStringArgs.init) {
|
||||||
|
// actually doing this is pretty painful, so gonna pawn it off on the C lib
|
||||||
|
import core.stdc.stdio;
|
||||||
|
// FIXME: what if there's a locale in place that changes the decimal point?
|
||||||
|
auto ret = snprintf(buffer.ptr, buffer.length, args.useScientificNotation ? "%.*e" : "%.*f", args.maximumPrecision, value);
|
||||||
|
if(!args.useScientificNotation && (args.padTo || args.groupSize)) {
|
||||||
|
char[32] scratch = void;
|
||||||
|
auto idx = buffer[0 .. ret].indexOf(".");
|
||||||
|
|
||||||
|
int digitsOutput = 0;
|
||||||
|
int digitsGrouped = 0;
|
||||||
|
if(idx > 0) {
|
||||||
|
// there is a whole number component
|
||||||
|
int pos = cast(int) scratch.length;
|
||||||
|
|
||||||
|
auto splitPoint = idx;
|
||||||
|
|
||||||
|
while(idx) {
|
||||||
|
if(args.groupSize && digitsGrouped == args.groupSize) {
|
||||||
|
scratch[--pos] = args.separator;
|
||||||
|
digitsGrouped = 0;
|
||||||
|
}
|
||||||
|
scratch[--pos] = buffer[--idx];
|
||||||
|
|
||||||
|
digitsOutput++;
|
||||||
|
digitsGrouped++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(args.padTo)
|
||||||
|
while(digitsOutput < args.padTo) {
|
||||||
|
if(args.groupSize && digitsGrouped == args.groupSize) {
|
||||||
|
scratch[--pos] = args.separator;
|
||||||
|
digitsGrouped = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
scratch[--pos] = args.padWith;
|
||||||
|
|
||||||
|
digitsOutput++;
|
||||||
|
digitsGrouped++;
|
||||||
|
}
|
||||||
|
|
||||||
|
char[32] remainingBuffer;
|
||||||
|
remainingBuffer[0 .. ret - splitPoint]= buffer[splitPoint .. ret];
|
||||||
|
|
||||||
|
buffer[0 .. scratch.length - pos] = scratch[pos .. $];
|
||||||
|
buffer[scratch.length - pos .. scratch.length - pos + ret - splitPoint] = remainingBuffer[0 .. ret - splitPoint];
|
||||||
|
|
||||||
|
ret = cast(int) scratch.length - pos + ret - splitPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FIXME: if maximum precision....?
|
||||||
|
return buffer[0 .. ret];
|
||||||
|
}
|
||||||
|
|
||||||
unittest {
|
unittest {
|
||||||
char[32] buffer;
|
char[32] buffer;
|
||||||
assert(intToString(0, buffer[]) == "0");
|
assert(intToString(0, buffer[]) == "0");
|
||||||
|
|
@ -1638,6 +1750,24 @@ unittest {
|
||||||
assert(intToString(0xef1, buffer[], IntToStringArgs().withRadix(16).withPadding(8)) == "00000ef1");
|
assert(intToString(0xef1, buffer[], IntToStringArgs().withRadix(16).withPadding(8)) == "00000ef1");
|
||||||
assert(intToString(-0xef1, buffer[], IntToStringArgs().withRadix(16).withPadding(8)) == "-00000ef1");
|
assert(intToString(-0xef1, buffer[], IntToStringArgs().withRadix(16).withPadding(8)) == "-00000ef1");
|
||||||
assert(intToString(-0xef1, buffer[], IntToStringArgs().withRadix(16, 'A').withPadding(8, ' ')) == "- EF1");
|
assert(intToString(-0xef1, buffer[], IntToStringArgs().withRadix(16, 'A').withPadding(8, ' ')) == "- EF1");
|
||||||
|
|
||||||
|
assert(intToString(4000, buffer[], IntToStringArgs().withPadding(4).withGroupSeparator(3, ',')) == "4,000");
|
||||||
|
assert(intToString(400, buffer[], IntToStringArgs().withPadding(4).withGroupSeparator(3, ',')) == "0,400");
|
||||||
|
|
||||||
|
const pi = 3.14159256358979;
|
||||||
|
assert(floatToString(pi, buffer[], FloatToStringArgs().withPrecision(3)) == "3.142");
|
||||||
|
assert(floatToString(pi, buffer[], FloatToStringArgs().withPrecision(2)) == "3.14");
|
||||||
|
assert(floatToString(pi, buffer[], FloatToStringArgs().withPrecision(0)) == "3");
|
||||||
|
|
||||||
|
assert(floatToString(4.0, buffer[], FloatToStringArgs().withPrecision(0)) == "4");
|
||||||
|
assert(floatToString(4.0, buffer[], FloatToStringArgs().withPrecision(3)) == "4.000");
|
||||||
|
|
||||||
|
assert(floatToString(4.0, buffer[], FloatToStringArgs().withPadding(3).withPrecision(3)) == "004.000");
|
||||||
|
assert(floatToString(4.0, buffer[], FloatToStringArgs().withPadding(3).withGroupSeparator(3, ',').withPrecision(3)) == "004.000");
|
||||||
|
assert(floatToString(4.0, buffer[], FloatToStringArgs().withPadding(4).withGroupSeparator(3, ',').withPrecision(3)) == "0,004.000");
|
||||||
|
assert(floatToString(4000.0, buffer[], FloatToStringArgs().withPadding(4).withGroupSeparator(3, ',').withPrecision(3)) == "4,000.000");
|
||||||
|
|
||||||
|
assert(floatToString(pi*10, buffer[], FloatToStringArgs().withPrecision(2).withScientificNotation(true)) == "3.14e+01");
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
|
|
@ -1693,7 +1823,9 @@ inout(char)[] stripRightInternal(return inout(char)[] s) {
|
||||||
Moved from color.d to core.d in March 2023 (dub v11.0).
|
Moved from color.d to core.d in March 2023 (dub v11.0).
|
||||||
+/
|
+/
|
||||||
string toStringInternal(T)(T t) {
|
string toStringInternal(T)(T t) {
|
||||||
char[32] buffer;
|
return writeGuts(null, null, null, false, &makeString, t);
|
||||||
|
/+
|
||||||
|
char[64] buffer;
|
||||||
static if(is(typeof(t.toString) : string))
|
static if(is(typeof(t.toString) : string))
|
||||||
return t.toString();
|
return t.toString();
|
||||||
else static if(is(T : string))
|
else static if(is(T : string))
|
||||||
|
|
@ -1726,6 +1858,7 @@ string toStringInternal(T)(T t) {
|
||||||
static assert(0, T.stringof ~ " makes compile too slow");
|
static assert(0, T.stringof ~ " makes compile too slow");
|
||||||
// import std.conv; return to!string(t);
|
// import std.conv; return to!string(t);
|
||||||
}
|
}
|
||||||
|
+/
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
|
|
@ -4015,20 +4148,51 @@ else class AsyncFile {
|
||||||
|
|
||||||
Tip: prefer the callback ones. If settings where async is possible, it will do async, and if not, it will sync.
|
Tip: prefer the callback ones. If settings where async is possible, it will do async, and if not, it will sync.
|
||||||
|
|
||||||
NOT IMPLEMENTED
|
NOT FULLY IMPLEMENTED
|
||||||
+/
|
+/
|
||||||
void writeFile(string filename, const(void)[] contents) {
|
void writeFile(string filename, const(void)[] contents) {
|
||||||
|
// FIXME: stop using the C lib and start error checking
|
||||||
}
|
import core.stdc.stdio;
|
||||||
|
CharzBuffer fn = filename;
|
||||||
/// ditto
|
auto file = fopen(fn.ptr, "wb");
|
||||||
string readTextFile(string filename, string fileEncoding = null) {
|
if(file is null)
|
||||||
return null;
|
throw new ErrnoApiException("fopen", errno, [SavedArgument("filename", LimitedVariant(filename))]);
|
||||||
|
fwrite(contents.ptr, 1, contents.length, file);
|
||||||
|
fclose(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ditto
|
/// ditto
|
||||||
const(ubyte[]) readBinaryFile(string filename) {
|
const(ubyte[]) readBinaryFile(string filename) {
|
||||||
return null;
|
// FIXME: stop using the C lib and check for more errors
|
||||||
|
|
||||||
|
import core.stdc.stdio;
|
||||||
|
CharzBuffer fn = filename;
|
||||||
|
auto file = fopen(fn.ptr, "rb");
|
||||||
|
if(file is null)
|
||||||
|
throw new ErrnoApiException("fopen", errno, [SavedArgument("filename", LimitedVariant(filename))]);
|
||||||
|
ubyte[] buffer = new ubyte[](64 * 1024);
|
||||||
|
ubyte[] contents;
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
auto ret = fread(buffer.ptr, 1, buffer.length, file);
|
||||||
|
if(ret < buffer.length) {
|
||||||
|
if(contents is null)
|
||||||
|
contents = buffer[0 .. ret];
|
||||||
|
else
|
||||||
|
contents ~= buffer[0 .. ret];
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
contents ~= buffer[0 .. ret];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ditto
|
||||||
|
string readTextFile(string filename, string fileEncoding = null) {
|
||||||
|
return cast(string) readBinaryFile(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
/+
|
/+
|
||||||
|
|
@ -8613,7 +8777,7 @@ shared(LoggerOf!GenericEmbeddableInterpolatedSequence) logger() {
|
||||||
Added April 17, 2025
|
Added April 17, 2025
|
||||||
+/
|
+/
|
||||||
void logSwallowedException(Exception e) {
|
void logSwallowedException(Exception e) {
|
||||||
logger.error(i"$(e.toString())");
|
logger.error(InterpolationHeader(), e.toString(), InterpolationFooter());
|
||||||
}
|
}
|
||||||
|
|
||||||
/+
|
/+
|
||||||
|
|
@ -8883,12 +9047,35 @@ private void appendToBuffer(ref char[] buffer, ref int pos, scope const(char)[]
|
||||||
}
|
}
|
||||||
|
|
||||||
private void appendToBuffer(ref char[] buffer, ref int pos, long what) {
|
private void appendToBuffer(ref char[] buffer, ref int pos, long what) {
|
||||||
if(buffer.length < pos + 16)
|
if(buffer.length < pos + 32)
|
||||||
buffer.length = pos + 16;
|
buffer.length = pos + 32;
|
||||||
auto sliced = intToString(what, buffer[pos .. $]);
|
auto sliced = intToString(what, buffer[pos .. $]);
|
||||||
pos += sliced.length;
|
pos += sliced.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void appendToBuffer(ref char[] buffer, ref int pos, double what) {
|
||||||
|
if(buffer.length < pos + 32)
|
||||||
|
buffer.length = pos + 32;
|
||||||
|
auto sliced = floatToString(what, buffer[pos .. $]);
|
||||||
|
pos += sliced.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/++
|
||||||
|
You can use `mixin(dumpParams);` to put out a debug print of your current function call w/ params.
|
||||||
|
+/
|
||||||
|
enum string dumpParams = q{
|
||||||
|
{
|
||||||
|
import arsd.core;
|
||||||
|
arsd.core.dumpParamsImpl(__FUNCTION__, __traits(parameters));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Don't call this directly, use `mixin(dumpParams);` instead
|
||||||
|
public void dumpParamsImpl(T...)(string func, T args) {
|
||||||
|
writeGuts(func ~ "(", ")\n", ", ", false, &actuallyWriteToStdout, args);
|
||||||
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
A `writeln` that actually works, at least for some basic types.
|
A `writeln` that actually works, at least for some basic types.
|
||||||
|
|
||||||
|
|
@ -8896,16 +9083,52 @@ private void appendToBuffer(ref char[] buffer, ref int pos, long what) {
|
||||||
|
|
||||||
This always does text. See also WritableStream and WritableTextStream when they are implemented.
|
This always does text. See also WritableStream and WritableTextStream when they are implemented.
|
||||||
+/
|
+/
|
||||||
void writeln(bool printInterpolatedCode = false, T...)(T t) {
|
void writeln(T...)(T t) {
|
||||||
|
writeGuts(null, "\n", null, false, &actuallyWriteToStdout, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void writelnStderr(T...)(T t) {
|
||||||
|
writeGuts(null, "\n", null, false, &actuallyWriteToStderr, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
package(arsd) string enumNameForValue(T)(T t) {
|
||||||
|
switch(t) {
|
||||||
|
foreach(memberName; __traits(allMembers, T)) {
|
||||||
|
case __traits(getMember, T, memberName):
|
||||||
|
return memberName;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "<unknown>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/+
|
||||||
|
Purposes:
|
||||||
|
* debugging
|
||||||
|
* writing
|
||||||
|
* converting single value to string?
|
||||||
|
+/
|
||||||
|
private string writeGuts(T...)(string prefix, string suffix, string argSeparator, bool printInterpolatedCode, string function(scope char[] result) writer, T t) {
|
||||||
char[256] bufferBacking;
|
char[256] bufferBacking;
|
||||||
char[] buffer = bufferBacking[];
|
char[] buffer = bufferBacking[];
|
||||||
int pos;
|
int pos;
|
||||||
|
|
||||||
foreach(arg; t) {
|
if(prefix.length)
|
||||||
|
appendToBuffer(buffer, pos, prefix);
|
||||||
|
|
||||||
|
foreach(i, arg; t) {
|
||||||
|
static if(i)
|
||||||
|
if(argSeparator.length)
|
||||||
|
appendToBuffer(buffer, pos, argSeparator);
|
||||||
|
|
||||||
static if(is(typeof(arg) Base == enum)) {
|
static if(is(typeof(arg) Base == enum)) {
|
||||||
appendToBuffer(buffer, pos, typeof(arg).stringof);
|
appendToBuffer(buffer, pos, typeof(arg).stringof);
|
||||||
appendToBuffer(buffer, pos, ".");
|
appendToBuffer(buffer, pos, ".");
|
||||||
appendToBuffer(buffer, pos, toStringInternal(arg));
|
appendToBuffer(buffer, pos, enumNameForValue(arg));
|
||||||
appendToBuffer(buffer, pos, "(");
|
appendToBuffer(buffer, pos, "(");
|
||||||
appendToBuffer(buffer, pos, cast(Base) arg);
|
appendToBuffer(buffer, pos, cast(Base) arg);
|
||||||
appendToBuffer(buffer, pos, ")");
|
appendToBuffer(buffer, pos, ")");
|
||||||
|
|
@ -8916,13 +9139,9 @@ void writeln(bool printInterpolatedCode = false, T...)(T t) {
|
||||||
} else static if(is(typeof(arg) : long)) {
|
} else static if(is(typeof(arg) : long)) {
|
||||||
appendToBuffer(buffer, pos, arg);
|
appendToBuffer(buffer, pos, arg);
|
||||||
} else static if(is(typeof(arg) : double)) {
|
} else static if(is(typeof(arg) : double)) {
|
||||||
import core.stdc.stdio;
|
appendToBuffer(buffer, pos, arg);
|
||||||
char[128] fb;
|
|
||||||
auto count = snprintf(fb.ptr, fb.length, "%.4lf", arg);
|
|
||||||
|
|
||||||
appendToBuffer(buffer, pos, fb[0 .. count]);
|
|
||||||
} else static if(is(typeof(arg) == InterpolatedExpression!code, string code)) {
|
} else static if(is(typeof(arg) == InterpolatedExpression!code, string code)) {
|
||||||
static if(printInterpolatedCode) {
|
if(printInterpolatedCode) {
|
||||||
appendToBuffer(buffer, pos, code);
|
appendToBuffer(buffer, pos, code);
|
||||||
appendToBuffer(buffer, pos, " = ");
|
appendToBuffer(buffer, pos, " = ");
|
||||||
}
|
}
|
||||||
|
|
@ -8952,29 +9171,46 @@ void writeln(bool printInterpolatedCode = false, T...)(T t) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendToBuffer(buffer, pos, "\n");
|
if(suffix.length)
|
||||||
|
appendToBuffer(buffer, pos, suffix);
|
||||||
|
|
||||||
actuallyWriteToStdout(buffer[0 .. pos]);
|
return writer(buffer[0 .. pos]);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug void dump(T...)(T t, string file = __FILE__, size_t line = __LINE__) {
|
debug void dump(T...)(T t, string file = __FILE__, size_t line = __LINE__) {
|
||||||
writeln!true(file, ":", line, ": ", t);
|
string separator;
|
||||||
|
static if(T.length && is(T[0] == InterpolationHeader))
|
||||||
|
separator = null;
|
||||||
|
else
|
||||||
|
separator = "; ";
|
||||||
|
|
||||||
|
writeGuts(file ~ ":" ~ toStringInternal(line) ~ ": ", "\n", separator, true, &actuallyWriteToStdout, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void actuallyWriteToStdout(scope char[] buffer) @trusted {
|
private string makeString(scope char[] buffer) @safe {
|
||||||
|
return buffer.idup;
|
||||||
|
}
|
||||||
|
private string actuallyWriteToStdout(scope char[] buffer) @safe {
|
||||||
|
return actuallyWriteToStdHandle(1, buffer);
|
||||||
|
}
|
||||||
|
private string actuallyWriteToStderr(scope char[] buffer) @safe {
|
||||||
|
return actuallyWriteToStdHandle(2, buffer);
|
||||||
|
}
|
||||||
|
private string actuallyWriteToStdHandle(int whichOne, scope char[] buffer) @trusted {
|
||||||
version(UseStdioWriteln)
|
version(UseStdioWriteln)
|
||||||
{
|
{
|
||||||
import std.stdio;
|
import std.stdio;
|
||||||
writeln(buffer);
|
(whichOne == 1 ? stdout : stderr).writeln(buffer);
|
||||||
}
|
}
|
||||||
else version(Windows) {
|
else version(Windows) {
|
||||||
import core.sys.windows.wincon;
|
import core.sys.windows.wincon;
|
||||||
|
|
||||||
auto hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
auto h = whichOne == 1 ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE;
|
||||||
|
|
||||||
|
auto hStdOut = GetStdHandle(h);
|
||||||
if(hStdOut == null || hStdOut == INVALID_HANDLE_VALUE) {
|
if(hStdOut == null || hStdOut == INVALID_HANDLE_VALUE) {
|
||||||
AllocConsole();
|
AllocConsole();
|
||||||
hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
hStdOut = GetStdHandle(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(GetFileType(hStdOut) == FILE_TYPE_CHAR) {
|
if(GetFileType(hStdOut) == FILE_TYPE_CHAR) {
|
||||||
|
|
@ -8989,8 +9225,10 @@ private void actuallyWriteToStdout(scope char[] buffer) @trusted {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
import unix = core.sys.posix.unistd;
|
import unix = core.sys.posix.unistd;
|
||||||
unix.write(1, buffer.ptr, buffer.length);
|
unix.write(whichOne, buffer.ptr, buffer.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/+
|
/+
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
module arsd.file;
|
||||||
|
|
||||||
|
import arsd.core;
|
||||||
|
|
||||||
|
// file name stuff
|
||||||
|
alias FilePath = arsd.core.FilePath;
|
||||||
|
|
||||||
|
// the basics
|
||||||
|
alias writeFile = arsd.core.writeFile;
|
||||||
|
alias readTextFile = arsd.core.readTextFile;
|
||||||
|
alias readBinaryFile = arsd.core.readBinaryFile;
|
||||||
|
|
||||||
|
// read lines
|
||||||
|
|
||||||
|
// directory listing
|
||||||
|
alias getFiles = arsd.core.getFiles;
|
||||||
|
alias DirectoryWatcher = arsd.core.DirectoryWatcher;
|
||||||
|
// stat?
|
||||||
|
// exists?
|
||||||
|
// symlink?
|
||||||
|
// remove?
|
||||||
|
// rename?
|
||||||
|
// copy?
|
||||||
|
|
||||||
|
// file objects
|
||||||
|
// alias AsyncFile = arsd.core.AsyncFile;
|
||||||
|
|
||||||
|
/+
|
||||||
|
unittest {
|
||||||
|
writeFile("sample.txt", "this is a test file");
|
||||||
|
assert(readTextFile("sample.txt") == "this is a test file");
|
||||||
|
}
|
||||||
|
+/
|
||||||
9
http2.d
9
http2.d
|
|
@ -59,6 +59,15 @@ unittest {
|
||||||
version(arsd_http2_integration_test) main(); // exclude from docs
|
version(arsd_http2_integration_test) main(); // exclude from docs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/+
|
||||||
|
// arsd core is now default but you can opt out for a lil while
|
||||||
|
version(no_arsd_core) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
version=use_arsd_core;
|
||||||
|
}
|
||||||
|
+/
|
||||||
|
|
||||||
static import arsd.core;
|
static import arsd.core;
|
||||||
|
|
||||||
// FIXME: I think I want to disable sigpipe here too.
|
// FIXME: I think I want to disable sigpipe here too.
|
||||||
|
|
|
||||||
|
|
@ -486,6 +486,8 @@ class WebViewWidget_CEF : WebViewWidgetBase {
|
||||||
cef_window_info_t window_info;
|
cef_window_info_t window_info;
|
||||||
window_info.parent_window = containerWindow.nativeWindowHandle;
|
window_info.parent_window = containerWindow.nativeWindowHandle;
|
||||||
|
|
||||||
|
writeln(cast(long) containerWindow.nativeWindowHandle);
|
||||||
|
|
||||||
cef_string_t cef_url = cef_string_t(url);//"http://arsdnet.net/test.html");
|
cef_string_t cef_url = cef_string_t(url);//"http://arsdnet.net/test.html");
|
||||||
|
|
||||||
cef_browser_settings_t browser_settings;
|
cef_browser_settings_t browser_settings;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/++
|
||||||
|
String manipulation functions.
|
||||||
|
|
||||||
|
See_Also:
|
||||||
|
To get a substring, you can use the built-in array slice operator.
|
||||||
|
|
||||||
|
For reading various encodings into a standard string, see [arsd.characterencodings].
|
||||||
|
|
||||||
|
For converting things to and from strings, see [arsd.conv].
|
||||||
|
|
||||||
|
For sorting an array of strings, see... std.algorithm for now but maybe here later.
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added May 23, 2025
|
||||||
|
+/
|
||||||
|
module arsd.string;
|
||||||
|
|
||||||
|
static import arsd.core;
|
||||||
|
|
||||||
|
/// Public interface to arsd.core
|
||||||
|
alias startsWith = arsd.core.startsWith;
|
||||||
|
/// ditto
|
||||||
|
alias endsWith = arsd.core.endsWith;
|
||||||
|
/// ditto
|
||||||
|
alias indexOf = arsd.core.indexOf;
|
||||||
|
|
||||||
|
// replace? replaceFirst, replaceAll, replaceAny etc
|
||||||
|
|
||||||
|
// limitSize - truncates to the last code point under the given length of code units
|
||||||
|
|
||||||
|
/// Strips (aka trims) leading and/or trailing whitespace from the string.
|
||||||
|
alias strip = arsd.core.stripInternal;
|
||||||
|
/// ditto
|
||||||
|
@deprecated("D calls this `strip` instead") alias trim = strip;
|
||||||
|
|
||||||
|
/// ditto
|
||||||
|
alias stripRight = arsd.core.stripInternal;
|
||||||
|
/// ditto
|
||||||
|
@deprecated("D calls this `stripRight` instead") alias trimRight = stripRight;
|
||||||
|
|
||||||
|
// stripLeft? variants where you can list the chars to strip?
|
||||||
|
|
||||||
|
// ascii to upper, to lower, capitalize words, from camel case to dash separated
|
||||||
|
|
||||||
|
// ********* UTF **************
|
||||||
|
// utf8 stride and such?
|
||||||
|
// get the starting code unit of the given point in the string
|
||||||
|
// get the next code unit start after the given point (compare upstream popFront)
|
||||||
|
// iterate over a string putting a replacement char in any invalid utf 8 spot
|
||||||
|
|
||||||
|
// ********* C INTEROP **************
|
||||||
|
|
||||||
|
alias stringz = arsd.core.stringz;
|
||||||
|
// CharzBuffer
|
||||||
|
// WCharzBuffer
|
||||||
|
|
@ -991,7 +991,7 @@ public struct Selection {
|
||||||
// need to find the exact thing in here
|
// need to find the exact thing in here
|
||||||
|
|
||||||
auto hit = segment.textBeginOffset;
|
auto hit = segment.textBeginOffset;
|
||||||
auto ul = segment.upperLeft;
|
MeasurableFont.fnum ulx = segment.upperLeft.x;
|
||||||
|
|
||||||
bool found;
|
bool found;
|
||||||
auto txt = layouter.text[segment.textBeginOffset .. segment.textEndOffset];
|
auto txt = layouter.text[segment.textBeginOffset .. segment.textEndOffset];
|
||||||
|
|
@ -1001,11 +1001,11 @@ public struct Selection {
|
||||||
|
|
||||||
hit = segment.textBeginOffset + cast(int) idx;
|
hit = segment.textBeginOffset + cast(int) idx;
|
||||||
|
|
||||||
auto distanceToLeft = ul.x - idealX;
|
auto distanceToLeft = ulx - idealX;
|
||||||
if(distanceToLeft < 0) distanceToLeft = -distanceToLeft;
|
if(distanceToLeft < 0) distanceToLeft = -distanceToLeft;
|
||||||
if(distanceToLeft < bestHitDistance) {
|
if(distanceToLeft < bestHitDistance) {
|
||||||
bestHit = hit;
|
bestHit = hit;
|
||||||
bestHitDistance = distanceToLeft;
|
bestHitDistance = castFnumToCnum(distanceToLeft);
|
||||||
} else {
|
} else {
|
||||||
// getting further away = no help
|
// getting further away = no help
|
||||||
break;
|
break;
|
||||||
|
|
@ -1013,13 +1013,13 @@ public struct Selection {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// FIXME: I probably want something slightly different
|
// FIXME: I probably want something slightly different
|
||||||
if(ul.x >= idealX) {
|
if(ulx >= idealX) {
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ul.x += width;
|
ulx += width;
|
||||||
codepoint++;
|
codepoint++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1422,19 +1422,23 @@ class TextLayouter {
|
||||||
|
|
||||||
int codepointCounter = 0;
|
int codepointCounter = 0;
|
||||||
auto bb = segment.boundingBox;
|
auto bb = segment.boundingBox;
|
||||||
|
MeasurableFont.fnum widthSum = 0;
|
||||||
foreach(thing, dchar cp; text[segment.textBeginOffset .. segment.textEndOffset]) {
|
foreach(thing, dchar cp; text[segment.textBeginOffset .. segment.textEndOffset]) {
|
||||||
auto w = segmentsWidths[segmentIndex][codepointCounter];
|
auto w = segmentsWidths[segmentIndex][codepointCounter];
|
||||||
|
|
||||||
if(thing + segment.textBeginOffset == idx) {
|
if(thing + segment.textBeginOffset == idx) {
|
||||||
|
bb.left = castFnumToCnum(widthSum);
|
||||||
bb.right = cast(typeof(bb.right))(bb.left + w);
|
bb.right = cast(typeof(bb.right))(bb.left + w);
|
||||||
return bb;
|
return bb;
|
||||||
}
|
}
|
||||||
|
|
||||||
bb.left += w;
|
widthSum += w;
|
||||||
|
|
||||||
codepointCounter++;
|
codepointCounter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bb.left = castFnumToCnum(widthSum);
|
||||||
|
|
||||||
bb.right = bb.left + 1;
|
bb.right = bb.left + 1;
|
||||||
|
|
||||||
return bb;
|
return bb;
|
||||||
|
|
@ -1499,11 +1503,11 @@ class TextLayouter {
|
||||||
|
|
||||||
auto bb = boundingBoxOfGlyph(segmentIndex, selection.focus);
|
auto bb = boundingBoxOfGlyph(segmentIndex, selection.focus);
|
||||||
|
|
||||||
bb.top += glyphStyle.font.ascent;
|
bb.top += castFnumToCnum(glyphStyle.font.ascent);
|
||||||
bb.bottom -= glyphStyle.font.descent;
|
bb.bottom -= castFnumToCnum(glyphStyle.font.descent);
|
||||||
|
|
||||||
bb.top -= insertionStyle.font.ascent;
|
bb.top -= castFnumToCnum(insertionStyle.font.ascent);
|
||||||
bb.bottom += insertionStyle.font.descent;
|
bb.bottom += castFnumToCnum(insertionStyle.font.descent);
|
||||||
|
|
||||||
caretInformation[cic++] = CaretInformation(cast(int) si, bb);
|
caretInformation[cic++] = CaretInformation(cast(int) si, bb);
|
||||||
}
|
}
|
||||||
|
|
@ -1541,21 +1545,21 @@ class TextLayouter {
|
||||||
|
|
||||||
Rectangle bbOriginal = di.boundingBox;
|
Rectangle bbOriginal = di.boundingBox;
|
||||||
|
|
||||||
int segmentWidth;
|
MeasurableFont.fnum segmentWidth = 0;
|
||||||
|
|
||||||
foreach(width; segmentsWidths[segmentIndex][codepointStart .. codepointEnd]) {
|
foreach(width; segmentsWidths[segmentIndex][codepointStart .. codepointEnd]) {
|
||||||
segmentWidth += width;
|
segmentWidth += width;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto diFragment = di;
|
auto diFragment = di;
|
||||||
diFragment.boundingBox.right = diFragment.boundingBox.left + segmentWidth;
|
diFragment.boundingBox.right = castFnumToCnum(diFragment.boundingBox.left + segmentWidth);
|
||||||
|
|
||||||
// FIXME: adjust the rest of di for this
|
// FIXME: adjust the rest of di for this
|
||||||
// FIXME: the caretInformation arguably should be truncated for those not in this particular sub-segment
|
// FIXME: the caretInformation arguably should be truncated for those not in this particular sub-segment
|
||||||
exit = !dg(txt[start .. end], style, diFragment, caretInformation[0 .. cic]);
|
exit = !dg(txt[start .. end], style, diFragment, caretInformation[0 .. cic]);
|
||||||
|
|
||||||
di.initialBaseline.x += segmentWidth;
|
di.initialBaseline.x += castFnumToCnum(segmentWidth);
|
||||||
di.boundingBox.left += segmentWidth;
|
di.boundingBox.left += castFnumToCnum(segmentWidth);
|
||||||
|
|
||||||
lastSelPos = end;
|
lastSelPos = end;
|
||||||
lastSelCodepoint = codepointEnd;
|
lastSelCodepoint = codepointEnd;
|
||||||
|
|
@ -1717,7 +1721,7 @@ class TextLayouter {
|
||||||
int styleInformationIndex;
|
int styleInformationIndex;
|
||||||
|
|
||||||
// calculated values after iterating through the segment
|
// calculated values after iterating through the segment
|
||||||
MeasurableFont.fnum width; // short
|
MeasurableFont.fnum width = 0; // short
|
||||||
int height; // short
|
int height; // short
|
||||||
|
|
||||||
Point upperLeft;
|
Point upperLeft;
|
||||||
|
|
@ -1780,7 +1784,7 @@ class TextLayouter {
|
||||||
|
|
||||||
auto boundingBox = Rectangle(segment.upperLeft, Size(castFnumToCnum(segment.width), segment.height));
|
auto boundingBox = Rectangle(segment.upperLeft, Size(castFnumToCnum(segment.width), segment.height));
|
||||||
if(boundingBox.contains(p)) {
|
if(boundingBox.contains(p)) {
|
||||||
int x = segment.upperLeft.x;
|
MeasurableFont.fnum x = segment.upperLeft.x;
|
||||||
int codePointIndex = 0;
|
int codePointIndex = 0;
|
||||||
|
|
||||||
int bestHit = int.max;
|
int bestHit = int.max;
|
||||||
|
|
@ -1790,7 +1794,7 @@ class TextLayouter {
|
||||||
const width = segmentsWidths[segmentIndex][codePointIndex];
|
const width = segmentsWidths[segmentIndex][codePointIndex];
|
||||||
idx = segment.textBeginOffset + cast(int) i; // can't just idx++ since it needs utf-8 stride
|
idx = segment.textBeginOffset + cast(int) i; // can't just idx++ since it needs utf-8 stride
|
||||||
|
|
||||||
auto distanceToLeft = p.x - x;
|
auto distanceToLeft = castFnumToCnum(p.x - x);
|
||||||
if(distanceToLeft < 0) distanceToLeft = -distanceToLeft;
|
if(distanceToLeft < 0) distanceToLeft = -distanceToLeft;
|
||||||
|
|
||||||
//auto distanceToRight = p.x - (x + width);
|
//auto distanceToRight = p.x - (x + width);
|
||||||
|
|
@ -2214,7 +2218,7 @@ class TextLayouter {
|
||||||
auto thisLineY = currentCorner.y;
|
auto thisLineY = currentCorner.y;
|
||||||
|
|
||||||
auto thisLineHeight = lineHeight;
|
auto thisLineHeight = lineHeight;
|
||||||
currentCorner.y += lineHeight;
|
currentCorner.y += castFnumToCnum(lineHeight);
|
||||||
currentCorner.x = 0;
|
currentCorner.x = 0;
|
||||||
|
|
||||||
finishSegment(idx); // i use currentCorner in there! so this must be after that
|
finishSegment(idx); // i use currentCorner in there! so this must be after that
|
||||||
|
|
@ -2234,7 +2238,7 @@ class TextLayouter {
|
||||||
|
|
||||||
auto baseline = thisLineHeight - biggestDescent;
|
auto baseline = thisLineHeight - biggestDescent;
|
||||||
|
|
||||||
seg.upperLeft.y += baseline - font.ascent;
|
seg.upperLeft.y += castFnumToCnum(baseline - font.ascent);
|
||||||
seg.height = castFnumToCnum(thisLineHeight - (baseline - font.ascent));
|
seg.height = castFnumToCnum(thisLineHeight - (baseline - font.ascent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2415,7 +2419,7 @@ class TextLayouter {
|
||||||
thisWidth = 16 + tabStop - currentCorner.x % tabStop;
|
thisWidth = 16 + tabStop - currentCorner.x % tabStop;
|
||||||
|
|
||||||
segment.width += thisWidth;
|
segment.width += thisWidth;
|
||||||
currentCorner.x += thisWidth;
|
currentCorner.x += castFnumToCnum(thisWidth);
|
||||||
|
|
||||||
endSegment = true;
|
endSegment = true;
|
||||||
goto advance;
|
goto advance;
|
||||||
|
|
@ -2442,7 +2446,7 @@ class TextLayouter {
|
||||||
}
|
}
|
||||||
|
|
||||||
segment.width += thisWidth;
|
segment.width += thisWidth;
|
||||||
currentCorner.x += thisWidth;
|
currentCorner.x += castFnumToCnum(thisWidth);
|
||||||
|
|
||||||
version(try_kerning_hack) {
|
version(try_kerning_hack) {
|
||||||
lastWidth = thisWidth;
|
lastWidth = thisWidth;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
module arsd.uda;
|
||||||
|
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
Blueprint extractUdas(Blueprint, Udas...)(Blueprint defaults) {
|
||||||
|
foreach(alias uda; Udas) {
|
||||||
|
static if(is(typeof(uda) == Blueprint)) {
|
||||||
|
defaults = uda;
|
||||||
|
} else {
|
||||||
|
foreach(ref member; defaults.tupleof)
|
||||||
|
static if(is(typeof(member) == typeof(uda)))
|
||||||
|
member = uda;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
import core.attribute;
|
||||||
|
static struct Name {
|
||||||
|
@implicit this(string name) { this.name = name; }
|
||||||
|
string name;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct Priority {
|
||||||
|
@implicit this(int priority) { this.priority = priority; }
|
||||||
|
int priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct Blueprint {
|
||||||
|
Name name;
|
||||||
|
Priority priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class A {
|
||||||
|
@Name("a") int a;
|
||||||
|
@Priority(44) int b;
|
||||||
|
int c;
|
||||||
|
@Priority(33) @Name("d") int d;
|
||||||
|
// @(wtf => wtf) int e; // won't compile when trying to get the blueprint...
|
||||||
|
|
||||||
|
@Blueprint(name: "foo", priority: 44) int g;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bp2 = Blueprint(name: "foo", priority: 44);
|
||||||
|
|
||||||
|
foreach(memberName; __traits(derivedMembers, A)) {
|
||||||
|
alias member = __traits(getMember, A, memberName);
|
||||||
|
auto bp = extractUdas!(Blueprint, __traits(getAttributes, member))(Blueprint.init);
|
||||||
|
import std.stdio; writeln(memberName, " ", bp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/++
|
||||||
|
Future public interface to the Uri struct and encode/decode component functions.
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added May 26, 2025
|
||||||
|
+/
|
||||||
|
module arsd.uri;
|
||||||
|
|
||||||
|
import arsd.core;
|
||||||
|
|
||||||
|
alias encodeUriComponent = arsd.core.encodeUriComponent;
|
||||||
|
alias decodeUriComponent = arsd.core.decodeUriComponent;
|
||||||
|
|
||||||
|
// phobos compatibility names
|
||||||
|
alias encodeComponent = encodeUriComponent;
|
||||||
|
alias decodeComponent = decodeUriComponent;
|
||||||
|
|
||||||
|
// FIXME: merge and pull Uri struct from http2 and cgi. maybe via core.
|
||||||
|
|
||||||
|
// might also put base64 in here....
|
||||||
Loading…
Reference in New Issue