mirror of https://github.com/adamdruppe/arsd.git
Compare commits
19 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
f494fcf9ca | |
|
|
26acaaae99 | |
|
|
21e8be8dcc | |
|
|
de5c8055cf | |
|
|
6b3aba5374 | |
|
|
736477b128 | |
|
|
2ab06d1249 | |
|
|
cea860a447 | |
|
|
8da8d51a86 | |
|
|
bfc0014ae2 | |
|
|
779bcb0199 | |
|
|
e8cacb7277 | |
|
|
e73713c026 | |
|
|
c94cc54f43 | |
|
|
aa2e04e6ca | |
|
|
b651bc567e | |
|
|
8efd1ffa0e | |
|
|
b3d88cafa8 | |
|
|
f7c2cbc0d2 |
|
|
@ -98,6 +98,7 @@ import arsd.rtf;
|
||||||
// import arsd.screen; // D1 or 2.098
|
// import arsd.screen; // D1 or 2.098
|
||||||
import arsd.script;
|
import arsd.script;
|
||||||
import arsd.sha;
|
import arsd.sha;
|
||||||
|
import arsd.shell;
|
||||||
import arsd.simpleaudio;
|
import arsd.simpleaudio;
|
||||||
import arsd.simpledisplay;
|
import arsd.simpledisplay;
|
||||||
import arsd.sqlite;
|
import arsd.sqlite;
|
||||||
|
|
|
||||||
524
database.d
524
database.d
|
|
@ -109,6 +109,8 @@ interface Database {
|
||||||
import arsd.core;
|
import arsd.core;
|
||||||
Variant[] vargs;
|
Variant[] vargs;
|
||||||
string sql;
|
string sql;
|
||||||
|
|
||||||
|
// FIXME: use the new helper functions sqlFromInterpolatedArgs and variantsFromInterpolatedArgs
|
||||||
foreach(arg; args) {
|
foreach(arg; args) {
|
||||||
static if(is(typeof(arg) == InterpolatedLiteral!str, string str)) {
|
static if(is(typeof(arg) == InterpolatedLiteral!str, string str)) {
|
||||||
sql ~= str;
|
sql ~= str;
|
||||||
|
|
@ -123,10 +125,19 @@ interface Database {
|
||||||
}
|
}
|
||||||
return queryImpl(sql, vargs);
|
return queryImpl(sql, vargs);
|
||||||
}
|
}
|
||||||
|
// see test/dbis.d
|
||||||
|
|
||||||
/// turns a systime into a value understandable by the target database as a timestamp to be concated into a query. so it should be quoted and escaped etc as necessary
|
/// turns a systime into a value understandable by the target database as a timestamp to be concated into a query. so it should be quoted and escaped etc as necessary
|
||||||
string sysTimeToValue(SysTime);
|
string sysTimeToValue(SysTime);
|
||||||
|
|
||||||
|
/++
|
||||||
|
Return true if the connection appears to be alive
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added October 30, 2025
|
||||||
|
+/
|
||||||
|
bool isAlive();
|
||||||
|
|
||||||
/// Prepared statement api
|
/// Prepared statement api
|
||||||
/*
|
/*
|
||||||
PreparedStatement prepareStatement(string sql, int numberOfArguments);
|
PreparedStatement prepareStatement(string sql, int numberOfArguments);
|
||||||
|
|
@ -349,6 +360,519 @@ interface ResultSet {
|
||||||
/* deprecated */ final ResultSet byAssoc() { return this; }
|
/* deprecated */ final ResultSet byAssoc() { return this; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Converts a database result set to a html table, using [arsd.dom].
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added October 29, 2025
|
||||||
|
+/
|
||||||
|
auto resultSetToHtmlTable()(ResultSet resultSet) {
|
||||||
|
import arsd.dom;
|
||||||
|
|
||||||
|
Table table = cast(Table) Element.make("table");
|
||||||
|
table.appendHeaderRow(resultSet.fieldNames);
|
||||||
|
foreach(row; resultSet) {
|
||||||
|
table.appendRow(row.toStringArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ConnectionPoolBase : arsd.core.SynchronizableObject {
|
||||||
|
protected struct DatabaseListItem {
|
||||||
|
Database db;
|
||||||
|
DatabaseListItem* nextAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DatabaseListItem* firstAvailable;
|
||||||
|
|
||||||
|
// FIXME: add a connection count limit and some kind of wait mechanism for one to become available
|
||||||
|
|
||||||
|
final protected void makeAvailable(DatabaseListItem* what) {
|
||||||
|
synchronized(this) {
|
||||||
|
auto keep = this.firstAvailable;
|
||||||
|
what.nextAvailable = keep;
|
||||||
|
this.firstAvailable = what;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final protected DatabaseListItem* getNext() {
|
||||||
|
DatabaseListItem* toUse;
|
||||||
|
synchronized(this) {
|
||||||
|
if(this.firstAvailable !is null) {
|
||||||
|
toUse = this.firstAvailable;
|
||||||
|
this.firstAvailable = this.firstAvailable.nextAvailable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toUse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
PooledConnection is an RAII holder for a database connection that is automatically recycled to the pool it came from (unless you [discard] it).
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added October 29, 2025
|
||||||
|
+/
|
||||||
|
struct PooledConnection(ConnectionPoolType) {
|
||||||
|
private ConnectionPoolType.DatabaseListItem* dli;
|
||||||
|
private ConnectionPoolType pool;
|
||||||
|
private bool discarded;
|
||||||
|
private this(ConnectionPoolType.DatabaseListItem* dli, ConnectionPoolType pool) {
|
||||||
|
this.dli = dli;
|
||||||
|
this.pool = pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
@disable this(this);
|
||||||
|
|
||||||
|
/++
|
||||||
|
Indicates you want the connection discarded instead of returned to the pool when you're finished with it.
|
||||||
|
|
||||||
|
You should call this if you know the connection is dead.
|
||||||
|
+/
|
||||||
|
void discard() {
|
||||||
|
this.discarded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~this() {
|
||||||
|
import core.memory;
|
||||||
|
if(GC.inFinalizer)
|
||||||
|
return;
|
||||||
|
if(!discarded && dli.db.isAlive) {
|
||||||
|
// FIXME: a connection must not be returned to the pool unless it is both alive and idle; any pending query work would screw up the next user
|
||||||
|
// it is the user's responsibility to live with other state though like prepared statements or whatever saved per-connection.
|
||||||
|
pool.makeAvailable(dli);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
ConnectionPoolType.DriverType borrow() @system return {
|
||||||
|
return cast(ConnectionPoolType.DriverType) dli.db; // static_cast
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+/
|
||||||
|
ResultSet rtQuery(T...)(T t) {
|
||||||
|
return dli.db.query(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
template query(string file = __FILE__, size_t line = __LINE__, Args...) {
|
||||||
|
enum asSql = sqlFromInterpolatedArgs!(Args);
|
||||||
|
__gshared queryMetadata = new QueryMetadata!(asSql, file, line);
|
||||||
|
@(arsd.core.standalone) @system shared static this() {
|
||||||
|
ConnectionPoolType.registeredQueries_ ~= queryMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto query(arsd.core.InterpolationHeader ihead, Args args, arsd.core.InterpolationFooter ifoot) {
|
||||||
|
return new QueryResult!queryMetadata(dli.db.queryImpl(asSql, variantsFromInterpolatedArgs(args)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
unittest {
|
||||||
|
import arsd.database;
|
||||||
|
|
||||||
|
shared dbPool = new shared ConnectionPool!(() => new MockDatabase())();
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
auto db = dbPool.get();
|
||||||
|
foreach(row; db.query(i"SELECT * FROM test")) {
|
||||||
|
if(row.id.isNull)
|
||||||
|
continue;
|
||||||
|
auto id = row.id.get!int;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
main(); // remove from docs
|
||||||
|
}
|
||||||
|
|
||||||
|
private Variant[] variantsFromInterpolatedArgs(Args...)(Args args) {
|
||||||
|
Variant[] ret;
|
||||||
|
|
||||||
|
import arsd.core;
|
||||||
|
|
||||||
|
foreach(arg; args) {
|
||||||
|
static if(is(typeof(arg) == InterpolationHeader))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) == InterpolationFooter))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) == InterpolatedLiteral!sql, string sql))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) == InterpolatedExpression!code, string code))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) == AdHocBuiltStruct!(tag, names, Values), string tag, string[] names, Values...)) {
|
||||||
|
static if(tag == "VALUES") {
|
||||||
|
foreach(value; arg.values) {
|
||||||
|
static if(is(value == sql_!code, string code)) {
|
||||||
|
// intentionally blank
|
||||||
|
} else {
|
||||||
|
ret ~= Variant(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else static assert(0);
|
||||||
|
}
|
||||||
|
// FIXME: iraw and sql!"" too and VALUES
|
||||||
|
else
|
||||||
|
ret ~= Variant(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string sqlFromInterpolatedArgs(Args...)() {
|
||||||
|
string ret;
|
||||||
|
|
||||||
|
import arsd.core;
|
||||||
|
|
||||||
|
foreach(arg; Args) {
|
||||||
|
static if(is(arg == InterpolationHeader))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(arg == InterpolationFooter))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(arg == InterpolatedLiteral!sql, string sql))
|
||||||
|
ret ~= sql;
|
||||||
|
else
|
||||||
|
static if(is(arg == InterpolatedExpression!code, string code))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(arg == AdHocBuiltStruct!(tag, names, values), string tag, string[] names, values...)) {
|
||||||
|
static if(tag == "VALUES") {
|
||||||
|
ret ~= "(";
|
||||||
|
foreach(idx, name; names) {
|
||||||
|
if(idx) ret ~= ", ";
|
||||||
|
ret ~= name;
|
||||||
|
}
|
||||||
|
ret ~= ") VALUES (";
|
||||||
|
foreach(idx, value; values) {
|
||||||
|
if(idx) ret ~= ", ";
|
||||||
|
|
||||||
|
static if(is(value == sql_!code, string code)) {
|
||||||
|
ret ~= code;
|
||||||
|
} else {
|
||||||
|
ret ~= "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret ~= ")";
|
||||||
|
}
|
||||||
|
else static assert(0);
|
||||||
|
}
|
||||||
|
// FIXME: iraw and sql_!"" too
|
||||||
|
else
|
||||||
|
ret ~= "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/+
|
||||||
|
+/
|
||||||
|
|
||||||
|
struct AssociatedDatabaseDatum(alias queryMetadata, string name, string file, size_t line) {
|
||||||
|
@(arsd.core.standalone) @system shared static this() {
|
||||||
|
queryMetadata.registerName(name, file, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
template get(T, string file = __FILE__, size_t line = __LINE__) {
|
||||||
|
shared static this() {
|
||||||
|
// FIXME: empty string and null must be distinguishable in arsd.core
|
||||||
|
static if(is(T == string))
|
||||||
|
T t = "sample";
|
||||||
|
else
|
||||||
|
T t = T.init;
|
||||||
|
queryMetadata.registerType(name, LimitedVariant(t), T.stringof, file, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
T get() {
|
||||||
|
import std.conv;
|
||||||
|
return datum.toString().to!T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseDatum datum;
|
||||||
|
|
||||||
|
bool isNull() {
|
||||||
|
return datum.isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
string toString() {
|
||||||
|
if(isNull)
|
||||||
|
return null;
|
||||||
|
else
|
||||||
|
return datum.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
alias toString this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract class QueryResultBase {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class QueryResult(alias queryMetadata) : QueryResultBase {
|
||||||
|
private ResultSet resultSet;
|
||||||
|
|
||||||
|
this(ResultSet resultSet) {
|
||||||
|
this.resultSet = resultSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResultRow!queryMetadata front() {
|
||||||
|
return new QueryResultRow!queryMetadata(resultSet.front);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool empty() {
|
||||||
|
return resultSet.empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
void popFront() {
|
||||||
|
resultSet.popFront();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QueryResultRow(alias queryMetadata) {
|
||||||
|
Row row;
|
||||||
|
this(Row row) {
|
||||||
|
this.row = row;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssociatedDatabaseDatum!(queryMetadata, name, file, line) opDispatch(string name, string file = __FILE__, size_t line = __LINE__)() if(name != "__dtor") {
|
||||||
|
return typeof(return)(row[name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// i could support an opSlice. maybe opIndex tho it won't be CT bound checked w/o a ct!0 thing
|
||||||
|
// also want opApply which discards type check prolly and just gives you the datum.
|
||||||
|
|
||||||
|
int opApply(int delegate(string, DatabaseDatum) dg) {
|
||||||
|
string[] fn = row.resultSet.fieldNames();
|
||||||
|
foreach(idx, item; row.row)
|
||||||
|
mixin(yield("fn[idx], item"));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ditto
|
||||||
|
int opApply(int delegate(DatabaseDatum) dg) {
|
||||||
|
foreach(item; row.row)
|
||||||
|
mixin(yield("item"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ReferencedColumn {
|
||||||
|
string name;
|
||||||
|
LimitedVariant sampleData;
|
||||||
|
string assumedType;
|
||||||
|
string actualType;
|
||||||
|
string file;
|
||||||
|
size_t line;
|
||||||
|
}
|
||||||
|
|
||||||
|
class QueryMetadataBase {
|
||||||
|
ReferencedColumn[] names;
|
||||||
|
void registerName(string name, string file, size_t line) {
|
||||||
|
names ~= ReferencedColumn(name, LimitedVariant.init, null, null, file, line);
|
||||||
|
}
|
||||||
|
void registerType(string name, LimitedVariant sample, string type, string file, size_t line) {
|
||||||
|
foreach(ref n; names)
|
||||||
|
if(n.name == name) {
|
||||||
|
if(n.assumedType.length && type.length) {
|
||||||
|
n.actualType = type;
|
||||||
|
}
|
||||||
|
n.assumedType = type;
|
||||||
|
n.sampleData = sample;
|
||||||
|
n.file = file;
|
||||||
|
n.line = line;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
names ~= ReferencedColumn(name, sample, type, type, file, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract string sql() const;
|
||||||
|
abstract string file() const;
|
||||||
|
abstract size_t line() const;
|
||||||
|
}
|
||||||
|
|
||||||
|
class QueryMetadata(string q, string file_, size_t line_) : QueryMetadataBase {
|
||||||
|
override string sql() const { return q; }
|
||||||
|
override string file() const { return file_; }
|
||||||
|
override size_t line() const { return line_; }
|
||||||
|
}
|
||||||
|
|
||||||
|
version(unittest)
|
||||||
|
class MockDatabase : Database {
|
||||||
|
void startTransaction() {}
|
||||||
|
string sysTimeToValue(SysTime s) { return null; }
|
||||||
|
bool isAlive() { return true; }
|
||||||
|
|
||||||
|
ResultSet queryImpl(string sql, Variant[] args...) {
|
||||||
|
return new PredefinedResultSet(null, null);
|
||||||
|
}
|
||||||
|
string escape(string sqlData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
string escapeBinaryString(const(ubyte)[] sqlData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Helpers for interpolated queries.
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added October 31, 2025
|
||||||
|
|
||||||
|
See_Also:
|
||||||
|
[arsd.core.iraw]
|
||||||
|
+/
|
||||||
|
auto VALUES() {
|
||||||
|
import arsd.core;
|
||||||
|
return AdHocBuiltStruct!"VALUES"();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ditto
|
||||||
|
auto sql(string s)() {
|
||||||
|
return sql_!s();
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct sql_(string s) { }
|
||||||
|
|
||||||
|
/++
|
||||||
|
A ConnectionPool manages a set of shared connections to a database.
|
||||||
|
|
||||||
|
|
||||||
|
Create one like this:
|
||||||
|
|
||||||
|
---
|
||||||
|
// at top level
|
||||||
|
shared dbPool = new shared ConnectionPool!(() => new PostgreSql("dbname=me"))();
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
auto db = dbPool.get(); // in the function, get it and use it temporarily
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added October 29, 2025
|
||||||
|
+/
|
||||||
|
class ConnectionPool(alias connectionFactory) : ConnectionPoolBase {
|
||||||
|
private alias unsharedThis = ConnectionPool!connectionFactory;
|
||||||
|
|
||||||
|
static if(is(typeof(connectionFactory) DriverType == return)) {
|
||||||
|
static if(!is(DriverType : Database))
|
||||||
|
static assert(0, "unusable connectionFactory - it needs to return an instance of Database");
|
||||||
|
} else {
|
||||||
|
static assert(0, "unusable connectionFactory - it needs to be a function");
|
||||||
|
}
|
||||||
|
|
||||||
|
private __gshared QueryMetadataBase[] registeredQueries_;
|
||||||
|
|
||||||
|
immutable(QueryMetadataBase[]) registeredQueries() shared {
|
||||||
|
return cast(immutable(QueryMetadataBase[])) registeredQueries_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkQueries()(DriverType db) shared {
|
||||||
|
bool succeeded = true;
|
||||||
|
|
||||||
|
import arsd.postgres; // FIXME is this really postgres only? looks like sqlite has no similar function... maybe make a view then sqlite3_table_column_metadata ?
|
||||||
|
static assert(is(DriverType == PostgreSql), "Only implemented for postgres right now");
|
||||||
|
|
||||||
|
int count;
|
||||||
|
import arsd.core;
|
||||||
|
import arsd.conv;
|
||||||
|
foreach(q; registeredQueries) {
|
||||||
|
//writeln(q.file, ":", q.line, " ", q.sql);
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
string dbSpecificSql;
|
||||||
|
int placeholderNumber = 1;
|
||||||
|
size_t lastCopied = 0;
|
||||||
|
foreach(idx, ch; q.sql) {
|
||||||
|
if(ch == '?') {
|
||||||
|
dbSpecificSql ~= q.sql[lastCopied .. idx];
|
||||||
|
lastCopied = idx + 1;
|
||||||
|
dbSpecificSql ~= "$" ~ to!string(placeholderNumber);
|
||||||
|
placeholderNumber++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dbSpecificSql ~= q.sql[lastCopied .. $];
|
||||||
|
// FIXME: pipeline this
|
||||||
|
db.query("PREPARE thing_"~to!string(++count)~" AS " ~ dbSpecificSql);
|
||||||
|
} catch(Exception e) {
|
||||||
|
e.file = q.file;
|
||||||
|
e.line = q.line;
|
||||||
|
throw e;
|
||||||
|
// continue;
|
||||||
|
}
|
||||||
|
// this mysql function looks about right: https://dev.mysql.com/doc/c-api/8.0/en/mysql-stmt-result-metadata.html
|
||||||
|
// could maybe emulate by trying it in a rolled back transaction though.
|
||||||
|
auto desca = describePrepared(db,"thing_"~arsd.conv.to!string(count));
|
||||||
|
LimitedVariant[string] byName;
|
||||||
|
foreach(col; desca.result) {
|
||||||
|
byName[col.fieldName] = col.type.storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(name; q.names) {
|
||||||
|
if(name.name !in byName)
|
||||||
|
throw ArsdException!"you reference unknown field"(name.name, name.file, name.line);
|
||||||
|
if(name.assumedType.length == 0)
|
||||||
|
continue;
|
||||||
|
if(byName[name.name].contains != name.sampleData.contains)
|
||||||
|
throw ArsdException!"type mismatch"(
|
||||||
|
name.name,
|
||||||
|
arsd.conv.to!string(byName[name.name].contains),
|
||||||
|
arsd.conv.to!string(name.sampleData.contains),
|
||||||
|
name.file,
|
||||||
|
name.line,
|
||||||
|
);
|
||||||
|
|
||||||
|
// i think this is redundant
|
||||||
|
if(name.assumedType.length && name.actualType.length && name.actualType != name.assumedType) {
|
||||||
|
throw ArsdException!"usage mismatch"(name.assumedType, name.actualType, name.file, name.line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
writeln(e.toString());
|
||||||
|
succeeded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!succeeded)
|
||||||
|
writeln("db check failed.");
|
||||||
|
|
||||||
|
return succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
public PooledConnection!(unsharedThis) get() shared {
|
||||||
|
auto toUse = (cast(unsharedThis) this).getNext();
|
||||||
|
|
||||||
|
if(toUse is null)
|
||||||
|
toUse = new DatabaseListItem(connectionFactory());
|
||||||
|
|
||||||
|
return PooledConnection!(unsharedThis)(toUse, cast(unsharedThis) this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class DatabaseException : Exception {
|
class DatabaseException : Exception {
|
||||||
this(string msg, string file = __FILE__, size_t line = __LINE__) {
|
this(string msg, string file = __FILE__, size_t line = __LINE__) {
|
||||||
super(msg, file, line);
|
super(msg, file, line);
|
||||||
|
|
|
||||||
43
dom.d
43
dom.d
|
|
@ -1,3 +1,5 @@
|
||||||
|
// FIXME: i want css nesting via the new standard now.
|
||||||
|
|
||||||
// FIXME: xml namespace support???
|
// FIXME: xml namespace support???
|
||||||
// FIXME: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
|
// FIXME: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
|
||||||
// FIXME: parentElement is parentNode that skips DocumentFragment etc but will be hard to work in with my compatibility...
|
// FIXME: parentElement is parentNode that skips DocumentFragment etc but will be hard to work in with my compatibility...
|
||||||
|
|
@ -2695,6 +2697,47 @@ class Element : DomParent {
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Makes an element from an interpolated sequence.
|
||||||
|
|
||||||
|
FIXME: add a type interpolator thing that can be replaced
|
||||||
|
FIXME: syntax check at compile time?
|
||||||
|
FIXME: allow a DocumentFragment in some cases
|
||||||
|
+/
|
||||||
|
static Element make(Args...)(arsd.core.InterpolationHeader head, Args args, arsd.core.InterpolationFooter foot) {
|
||||||
|
string html;
|
||||||
|
|
||||||
|
import arsd.core;
|
||||||
|
foreach(arg; args) {
|
||||||
|
static if(is(typeof(arg) == InterpolationHeader))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) == InterpolationFooter))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) == InterpolatedLiteral!h, string h))
|
||||||
|
html ~= h;
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) == InterpolatedExpression!code, string code))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) : iraw))
|
||||||
|
html ~= arg.s;
|
||||||
|
else
|
||||||
|
// FIXME: what if we are inside a <script> ? or an attribute etc
|
||||||
|
static if(is(typeof(arg) : Html))
|
||||||
|
html ~= arg.source;
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) : Element))
|
||||||
|
html ~= arg.toString();
|
||||||
|
else
|
||||||
|
html ~= htmlEntitiesEncode(toStringInternal(arg));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto root = Element.make("root");
|
||||||
|
root.innerHTML(html, true /* strict mode */);
|
||||||
|
return root.querySelector(" > *");
|
||||||
|
}
|
||||||
|
|
||||||
/// Generally, you don't want to call this yourself - use Element.make or document.createElement instead.
|
/// Generally, you don't want to call this yourself - use Element.make or document.createElement instead.
|
||||||
this(Document _parentDocument, string _tagName, string[string] _attributes = null, bool _selfClosed = false) {
|
this(Document _parentDocument, string _tagName, string[string] _attributes = null, bool _selfClosed = false) {
|
||||||
|
|
|
||||||
28
dub.json
28
dub.json
|
|
@ -11,10 +11,30 @@
|
||||||
"description": "Shared components across other arsd modules",
|
"description": "Shared components across other arsd modules",
|
||||||
"targetType": "library",
|
"targetType": "library",
|
||||||
"libs-windows": ["user32", "ws2_32"],
|
"libs-windows": ["user32", "ws2_32"],
|
||||||
"dflags-dmd": ["-mv=arsd.core=$PACKAGE_DIR/core.d"],
|
"dflags-dmd": [
|
||||||
"dflags-ldc": ["--mv=arsd.core=$PACKAGE_DIR/core.d"],
|
"-mv=arsd.core=$PACKAGE_DIR/core.d",
|
||||||
"dflags-gdc": ["-fmodule-file=arsd.core=$PACKAGE_DIR/core.d"],
|
"-mv=arsd.string=$PACKAGE_DIR/string.d",
|
||||||
"sourceFiles": ["core.d"]
|
"-mv=arsd.uri=$PACKAGE_DIR/uri.d",
|
||||||
|
"-mv=arsd.conv=$PACKAGE_DIR/conv.d"
|
||||||
|
],
|
||||||
|
"dflags-ldc": [
|
||||||
|
"--mv=arsd.core=$PACKAGE_DIR/core.d",
|
||||||
|
"--mv=arsd.string=$PACKAGE_DIR/string.d",
|
||||||
|
"--mv=arsd.uri=$PACKAGE_DIR/uri.d",
|
||||||
|
"--mv=arsd.conv=$PACKAGE_DIR/conv.d"
|
||||||
|
],
|
||||||
|
"dflags-gdc": [
|
||||||
|
"-fmodule-file=arsd.core=$PACKAGE_DIR/core.d",
|
||||||
|
"-fmodule-file=arsd.string=$PACKAGE_DIR/string.d",
|
||||||
|
"-fmodule-file=arsd.uri=$PACKAGE_DIR/uri.d",
|
||||||
|
"-fmodule-file=arsd.conv=$PACKAGE_DIR/conv.d"
|
||||||
|
],
|
||||||
|
"sourceFiles": [
|
||||||
|
"core.d",
|
||||||
|
"string.d",
|
||||||
|
"uri.d",
|
||||||
|
"conv.d"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "simpledisplay",
|
"name": "simpledisplay",
|
||||||
|
|
|
||||||
14
minigui.d
14
minigui.d
|
|
@ -17879,15 +17879,26 @@ class FilePicker : Dialog {
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
enum specialZoneSize = 1;
|
enum specialZoneSize = 1;
|
||||||
|
string originalString = whole;
|
||||||
|
bool fallback;
|
||||||
|
|
||||||
|
start_over:
|
||||||
|
|
||||||
char current = whole[0];
|
char current = whole[0];
|
||||||
if(current >= '0' && current <= '9') {
|
if(!fallback && current >= '0' && current <= '9') {
|
||||||
|
// if this overflows, it can mess up the sort, so it will fallback to string sort in that event
|
||||||
int accumulator;
|
int accumulator;
|
||||||
do {
|
do {
|
||||||
|
auto before = accumulator;
|
||||||
whole = whole[1 .. $];
|
whole = whole[1 .. $];
|
||||||
accumulator *= 10;
|
accumulator *= 10;
|
||||||
accumulator += current - '0';
|
accumulator += current - '0';
|
||||||
current = whole.length ? whole[0] : 0;
|
current = whole.length ? whole[0] : 0;
|
||||||
|
if(accumulator < before) {
|
||||||
|
fallback = true;
|
||||||
|
whole = originalString;
|
||||||
|
goto start_over;
|
||||||
|
}
|
||||||
} while (current >= '0' && current <= '9');
|
} while (current >= '0' && current <= '9');
|
||||||
|
|
||||||
return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols
|
return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols
|
||||||
|
|
@ -18277,6 +18288,7 @@ private FileType getFileType(string name) {
|
||||||
auto ret = stat((name ~ '\0').ptr, &buf);
|
auto ret = stat((name ~ '\0').ptr, &buf);
|
||||||
if(ret == -1)
|
if(ret == -1)
|
||||||
return FileType.error;
|
return FileType.error;
|
||||||
|
// FIXME: what about a symlink to a dir? S_IFLNK then readlink then i believe stat it again.
|
||||||
return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other;
|
return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other;
|
||||||
} else assert(0, "Not implemented");
|
} else assert(0, "Not implemented");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
4
mssql.d
4
mssql.d
|
|
@ -88,6 +88,10 @@ class MsSql : Database {
|
||||||
return null; // FIXME
|
return null; // FIXME
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override bool isAlive() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SQLHENV env;
|
SQLHENV env;
|
||||||
SQLHDBC conn;
|
SQLHDBC conn;
|
||||||
|
|
|
||||||
3
mysql.d
3
mysql.d
|
|
@ -219,6 +219,9 @@ class MySql : Database {
|
||||||
query("START TRANSACTION");
|
query("START TRANSACTION");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override bool isAlive() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
string sysTimeToValue(SysTime s) {
|
string sysTimeToValue(SysTime s) {
|
||||||
return "cast('" ~ escape(s.toISOExtString()) ~ "' as datetime)";
|
return "cast('" ~ escape(s.toISOExtString()) ~ "' as datetime)";
|
||||||
|
|
|
||||||
3
oauth.d
3
oauth.d
|
|
@ -2,7 +2,8 @@
|
||||||
module arsd.oauth;
|
module arsd.oauth;
|
||||||
|
|
||||||
import arsd.curl;
|
import arsd.curl;
|
||||||
import arsd.cgi; // for decodeVariables
|
import arsd.uri;
|
||||||
|
import arsd.cgi : Cgi;
|
||||||
import std.array;
|
import std.array;
|
||||||
static import std.uri;
|
static import std.uri;
|
||||||
static import std.algorithm;
|
static import std.algorithm;
|
||||||
|
|
|
||||||
106
postgres.d
106
postgres.d
|
|
@ -60,9 +60,12 @@ class PostgreSql : Database {
|
||||||
conn = PQconnectdb(toStringz(connectionString));
|
conn = PQconnectdb(toStringz(connectionString));
|
||||||
if(conn is null)
|
if(conn is null)
|
||||||
throw new DatabaseException("Unable to allocate PG connection object");
|
throw new DatabaseException("Unable to allocate PG connection object");
|
||||||
if(PQstatus(conn) != CONNECTION_OK)
|
if(PQstatus(conn) != CONNECTION_OK) {
|
||||||
|
this.connectionOk = false;
|
||||||
throw new DatabaseException(error());
|
throw new DatabaseException(error());
|
||||||
|
}
|
||||||
query("SET NAMES 'utf8'"); // D does everything with utf8
|
query("SET NAMES 'utf8'"); // D does everything with utf8
|
||||||
|
this.connectionOk = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
string connectionString;
|
string connectionString;
|
||||||
|
|
@ -75,6 +78,11 @@ class PostgreSql : Database {
|
||||||
return "'" ~ escape(s.toISOExtString()) ~ "'::timestamptz";
|
return "'" ~ escape(s.toISOExtString()) ~ "'::timestamptz";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool connectionOk;
|
||||||
|
override bool isAlive() {
|
||||||
|
return connectionOk;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Prepared statement support
|
Prepared statement support
|
||||||
|
|
||||||
|
|
@ -132,8 +140,10 @@ class PostgreSql : Database {
|
||||||
conn = PQconnectdb(toStringz(connectionString));
|
conn = PQconnectdb(toStringz(connectionString));
|
||||||
if(conn is null)
|
if(conn is null)
|
||||||
throw new DatabaseException("Unable to allocate PG connection object");
|
throw new DatabaseException("Unable to allocate PG connection object");
|
||||||
if(PQstatus(conn) != CONNECTION_OK)
|
if(PQstatus(conn) != CONNECTION_OK) {
|
||||||
|
this.connectionOk = false;
|
||||||
throw new DatabaseException(error());
|
throw new DatabaseException(error());
|
||||||
|
}
|
||||||
goto retry;
|
goto retry;
|
||||||
}
|
}
|
||||||
throw new DatabaseException(error());
|
throw new DatabaseException(error());
|
||||||
|
|
@ -179,6 +189,82 @@ class PostgreSql : Database {
|
||||||
PGconn* conn;
|
PGconn* conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/+
|
||||||
|
# when it changes from lowercase to upper case, call that a new word. or when it goes to/from anything else and underscore or dashes.
|
||||||
|
+/
|
||||||
|
|
||||||
|
struct PreparedStatementDescription {
|
||||||
|
PreparedStatementResult[] result;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PreparedStatementResult {
|
||||||
|
string fieldName;
|
||||||
|
DatabaseDatum type;
|
||||||
|
}
|
||||||
|
|
||||||
|
PreparedStatementDescription describePrepared(PostgreSql db, string name) {
|
||||||
|
auto res = PQdescribePrepared(db.conn, name.toStringz);
|
||||||
|
|
||||||
|
PreparedStatementResult[] ret;
|
||||||
|
|
||||||
|
// PQnparams PQparamtype for params
|
||||||
|
auto numFields = PQnfields(res);
|
||||||
|
foreach(num; 0 .. numFields) {
|
||||||
|
auto typeId = PQftype(res, num);
|
||||||
|
DatabaseDatum dd;
|
||||||
|
dd.platformSpecificTag = typeId;
|
||||||
|
dd.storage = sampleForOid(typeId);
|
||||||
|
ret ~= PreparedStatementResult(
|
||||||
|
copyCString(PQfname(res, num)),
|
||||||
|
dd,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PQclear(res);
|
||||||
|
|
||||||
|
return PreparedStatementDescription(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
import arsd.core : LimitedVariant, PackedDateTime, SimplifiedUtcTimestamp;
|
||||||
|
LimitedVariant sampleForOid(int platformSpecificTag) {
|
||||||
|
switch(platformSpecificTag) {
|
||||||
|
case BOOLOID:
|
||||||
|
return LimitedVariant(false);
|
||||||
|
case BYTEAOID:
|
||||||
|
return LimitedVariant(cast(const(ubyte)[]) null);
|
||||||
|
case TEXTOID:
|
||||||
|
case VARCHAROID:
|
||||||
|
return LimitedVariant("");
|
||||||
|
case INT4OID:
|
||||||
|
return LimitedVariant(0);
|
||||||
|
case INT8OID:
|
||||||
|
return LimitedVariant(0L);
|
||||||
|
case FLOAT4OID:
|
||||||
|
return LimitedVariant(0.0f);
|
||||||
|
case FLOAT8OID:
|
||||||
|
return LimitedVariant(0.0);
|
||||||
|
case TIMESTAMPOID:
|
||||||
|
case TIMESTAMPTZOID:
|
||||||
|
return LimitedVariant(SimplifiedUtcTimestamp(0));
|
||||||
|
case DATEOID:
|
||||||
|
PackedDateTime d;
|
||||||
|
d.hasDate = true;
|
||||||
|
return LimitedVariant(d); // might want a different type so contains shows the thing without checking hasDate and hasTime
|
||||||
|
case TIMETZOID: // possibly wrong... the tz isn't in my packed thing
|
||||||
|
case TIMEOID:
|
||||||
|
PackedDateTime d;
|
||||||
|
d.hasTime = true;
|
||||||
|
return LimitedVariant(d);
|
||||||
|
case INTERVALOID:
|
||||||
|
// months, days, and microseconds
|
||||||
|
|
||||||
|
case NUMERICOID: // aka decimal
|
||||||
|
default:
|
||||||
|
// when in doubt, assume it is just a string
|
||||||
|
return LimitedVariant("sample");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string toLowerFast(string s) {
|
private string toLowerFast(string s) {
|
||||||
import std.ascii : isUpper;
|
import std.ascii : isUpper;
|
||||||
foreach (c; s)
|
foreach (c; s)
|
||||||
|
|
@ -374,7 +460,22 @@ extern(C) {
|
||||||
int PQfformat(const PGresult *res, int column_number);
|
int PQfformat(const PGresult *res, int column_number);
|
||||||
|
|
||||||
alias Oid = int;
|
alias Oid = int;
|
||||||
|
enum BOOLOID = 16;
|
||||||
enum BYTEAOID = 17;
|
enum BYTEAOID = 17;
|
||||||
|
enum TEXTOID = 25;
|
||||||
|
enum INT4OID = 23; // integer
|
||||||
|
enum INT8OID = 20; // bigint
|
||||||
|
enum NUMERICOID = 1700;
|
||||||
|
enum FLOAT4OID = 700;
|
||||||
|
enum FLOAT8OID = 701;
|
||||||
|
enum VARCHAROID = 1043;
|
||||||
|
enum DATEOID = 1082;
|
||||||
|
enum TIMEOID = 1083;
|
||||||
|
enum TIMESTAMPOID = 1114;
|
||||||
|
enum TIMESTAMPTZOID = 1184;
|
||||||
|
enum INTERVALOID = 1186;
|
||||||
|
enum TIMETZOID = 1266;
|
||||||
|
|
||||||
Oid PQftype(const PGresult* res, int column_number);
|
Oid PQftype(const PGresult* res, int column_number);
|
||||||
|
|
||||||
char *PQescapeByteaConn(PGconn *conn,
|
char *PQescapeByteaConn(PGconn *conn,
|
||||||
|
|
@ -386,6 +487,7 @@ extern(C) {
|
||||||
|
|
||||||
char* PQcmdTuples(PGresult *res);
|
char* PQcmdTuples(PGresult *res);
|
||||||
|
|
||||||
|
PGresult *PQdescribePrepared(PGconn *conn, const char *stmtName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,839 @@
|
||||||
|
/++
|
||||||
|
Support functions to build a custom unix-style shell.
|
||||||
|
|
||||||
|
|
||||||
|
$(PITFALL
|
||||||
|
Do NOT use this to try to sanitize, escape, or otherwise parse what another shell would do with a string! Every shell is different and this implements my rules which may differ in subtle ways from any other common shell.
|
||||||
|
|
||||||
|
If you want to use this to understand a command, also use it to execute that command so you get what you expect.
|
||||||
|
)
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added October 18, 2025
|
||||||
|
|
||||||
|
Bugs:
|
||||||
|
$(LIST
|
||||||
|
* < and > redirections are not implemented at all
|
||||||
|
* >> not implemented
|
||||||
|
* | on Windows is not implemented
|
||||||
|
* glob expansion is minimal - * works, but no ?, no {item,other}, no {start..end}
|
||||||
|
* ~ expansion is not implemented
|
||||||
|
* `substitution` and $(...) is not implemented
|
||||||
|
* variable expansion is not implemented. can do $IDENT and ${IDENT} i think
|
||||||
|
* built-ins don't exist - `set`, want `for` and then like `export` and a way to hook in basic utilities polyfills especially on Windows (ls, rm, grep, etc)
|
||||||
|
* built-ins should have a pipe they can read/write to and return an int. integrate with arsd.cli?
|
||||||
|
* no !history recall. or history command in general
|
||||||
|
* job control is rudimentary - no fg, bg, jobs, &, ctrl+z, etc.
|
||||||
|
* set -o ignoreeof
|
||||||
|
* the path search is hardcoded
|
||||||
|
* prompt could be cooler
|
||||||
|
PS1 = normal prompt
|
||||||
|
PS2 = continuation prompt
|
||||||
|
Bash shell executes the content of the PROMPT_COMMAND just before displaying the PS1 variable.
|
||||||
|
|
||||||
|
bash does it with `\u` and stuff but i kinda thiink using `$USER` and such might make more sense.
|
||||||
|
* it prints command return values when you might not want that
|
||||||
|
* LS_COLORS env var is not set
|
||||||
|
* && and || is not implemented
|
||||||
|
* the api is not very good
|
||||||
|
* ulimit? sourcing things too. aliases.
|
||||||
|
* see my bash rc for other little things. maybe i want a deeshrc
|
||||||
|
* permission denied when hitting tab on Windows
|
||||||
|
* tab complete of available commands not implemented - get it from path search.
|
||||||
|
* some vars dynamic like $_ being the most recent command, $? being its return value, etc
|
||||||
|
)
|
||||||
|
|
||||||
|
Questionable_ideas:
|
||||||
|
$(LIST
|
||||||
|
* separate stdout and stderr more by default, allow stderr pipes.
|
||||||
|
* custom completion scripts? prolly not bash compatible since the scripts would be more involved
|
||||||
|
* some kind of scriptable cmdlet? a full on script language with shell stuff embeddable?
|
||||||
|
see https://hush-shell.github.io/cmd/index.html for some ok ideas
|
||||||
|
* do something fun with job control. idk what tho really.
|
||||||
|
* can terminal emulators get notifications when the foreground process group changes? i don't think so but i could make a "poll again now" sequence since i control shell and possibly terminal emulator now.
|
||||||
|
* change DISPLAY and such when attaching remote sessions
|
||||||
|
)
|
||||||
|
+/
|
||||||
|
module arsd.shell;
|
||||||
|
|
||||||
|
import arsd.core;
|
||||||
|
|
||||||
|
/++
|
||||||
|
Holds some context needed for shell expansions.
|
||||||
|
+/
|
||||||
|
struct ShellContext {
|
||||||
|
string[string] vars;
|
||||||
|
string cwd;
|
||||||
|
string[string] userHomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum QuoteStyle {
|
||||||
|
none,
|
||||||
|
nonExpanding, // 'thing'
|
||||||
|
expanding, // "thing"
|
||||||
|
subcommand, // `thing`
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
alias Globber = string[] delegate(string str, ShellContext context);
|
||||||
|
|
||||||
|
/++
|
||||||
|
Represents one component of a shell command line as a precursor to parsing.
|
||||||
|
+/
|
||||||
|
struct ShellLexeme {
|
||||||
|
string l;
|
||||||
|
QuoteStyle quoteStyle;
|
||||||
|
|
||||||
|
string toEscapedFormat() {
|
||||||
|
if(quoteStyle == QuoteStyle.nonExpanding) {
|
||||||
|
string ret;
|
||||||
|
ret.reserve(l.length);
|
||||||
|
foreach(ch; l) {
|
||||||
|
if(ch == '*')
|
||||||
|
ret ~= "\\*";
|
||||||
|
else
|
||||||
|
ret ~= ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
} else {
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/+
|
||||||
|
/++
|
||||||
|
The second thing should be have toSingleArg called on it
|
||||||
|
+/
|
||||||
|
EnvironmentPair toEnvironmentPair(ShellLexeme context) {
|
||||||
|
assert(quoteStyle == QuoteStyle.none);
|
||||||
|
|
||||||
|
size_t splitPoint = l.length;
|
||||||
|
foreach(size_t idx, char ch; l) {
|
||||||
|
if(ch == '=') {
|
||||||
|
splitPoint = idx;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(splitPoint != l.length) {
|
||||||
|
return EnvironmentPair(l[0 .. splitPoint], ShellLexeme(l[splitPoint + 1 .. $]));
|
||||||
|
} else {
|
||||||
|
return EnvironmentPair(null, ShellLexeme.init);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Expands variables but not globs while replacing quotes and such. Note it is NOT safe to pass an expanded single arg to another shell
|
||||||
|
+/
|
||||||
|
string toExpandedSingleArg(ShellContext context) {
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Returns the value as an argv array, after shell expansion of variables, tildes, and globs
|
||||||
|
|
||||||
|
Does NOT attempt to execute `subcommands`.
|
||||||
|
+/
|
||||||
|
string[] toExpandedArgs(ShellContext context, Globber globber) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
+/
|
||||||
|
|
||||||
|
/++
|
||||||
|
This function in pure in all but formal annotation; it does not interact with the outside world.
|
||||||
|
+/
|
||||||
|
ShellLexeme[] lexShellCommandLine(string commandLine) {
|
||||||
|
ShellLexeme[] ret;
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
consumingWhitespace,
|
||||||
|
readingWord,
|
||||||
|
readingSingleQuoted,
|
||||||
|
readingEscaped,
|
||||||
|
readingExpandingContextEscaped,
|
||||||
|
readingDoubleQuoted,
|
||||||
|
// FIXME: readingSubcommand for `thing`
|
||||||
|
readingComment,
|
||||||
|
}
|
||||||
|
|
||||||
|
State state = State.consumingWhitespace;
|
||||||
|
size_t first = commandLine.length;
|
||||||
|
|
||||||
|
void endWord() {
|
||||||
|
state = State.consumingWhitespace;
|
||||||
|
first = commandLine.length; // we'll rewind upon encountering the next word, if there is one
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(size_t idx, char ch; commandLine) {
|
||||||
|
again:
|
||||||
|
final switch(state) {
|
||||||
|
case State.consumingWhitespace:
|
||||||
|
switch(ch) {
|
||||||
|
case ' ':
|
||||||
|
// the arg separators should all be collapsed to exactly one
|
||||||
|
if(ret.length && !(ret[$-1].quoteStyle == QuoteStyle.none && ret[$-1].l == " "))
|
||||||
|
ret ~= ShellLexeme(" ");
|
||||||
|
continue;
|
||||||
|
case '#':
|
||||||
|
state = State.readingComment;
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
first = idx;
|
||||||
|
state = State.readingWord;
|
||||||
|
goto again;
|
||||||
|
}
|
||||||
|
case State.readingWord:
|
||||||
|
switch(ch) {
|
||||||
|
case '\'':
|
||||||
|
if(first != idx)
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||||
|
first = idx + 1;
|
||||||
|
state = State.readingSingleQuoted;
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
// a \ch can be treated as just a single quoted single char...
|
||||||
|
if(first != idx)
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||||
|
first = idx + 1;
|
||||||
|
state = State.readingEscaped;
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
if(first != idx)
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||||
|
first = idx + 1;
|
||||||
|
state = State.readingDoubleQuoted;
|
||||||
|
break;
|
||||||
|
case ' ':
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||||
|
ret ~= ShellLexeme(" "); // an argument separator
|
||||||
|
endWord();
|
||||||
|
continue;
|
||||||
|
case '|', '<', '>', '&':
|
||||||
|
if(first != idx)
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||||
|
ret ~= ShellLexeme(commandLine[idx .. idx + 1]); // shell special symbol
|
||||||
|
endWord();
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
// keep searching
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case State.readingComment:
|
||||||
|
if(ch == '\n') {
|
||||||
|
endWord();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case State.readingSingleQuoted:
|
||||||
|
switch(ch) {
|
||||||
|
case '\'':
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.nonExpanding);
|
||||||
|
endWord();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case State.readingDoubleQuoted:
|
||||||
|
switch(ch) {
|
||||||
|
case '"':
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.expanding);
|
||||||
|
endWord();
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
state = State.readingExpandingContextEscaped;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case State.readingEscaped:
|
||||||
|
if(ch >= 0x80 && ch <= 0xBF) {
|
||||||
|
// continuation byte
|
||||||
|
continue;
|
||||||
|
} else if(first == idx) {
|
||||||
|
// first byte, keep searching for continuations
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// same as if the user wrote the escaped character in single quotes
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.nonExpanding);
|
||||||
|
|
||||||
|
if(state == State.readingExpandingContextEscaped) {
|
||||||
|
state = State.readingDoubleQuoted;
|
||||||
|
first = idx;
|
||||||
|
} else {
|
||||||
|
endWord();
|
||||||
|
}
|
||||||
|
goto again;
|
||||||
|
}
|
||||||
|
case State.readingExpandingContextEscaped:
|
||||||
|
if(ch == '"') {
|
||||||
|
// the -1 trims out the \
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx - 1], QuoteStyle.expanding);
|
||||||
|
state = State.readingDoubleQuoted;
|
||||||
|
first = idx; // we need to INCLUDE the " itself
|
||||||
|
} else {
|
||||||
|
// this was actually nothing special, the backslash is kept in the double quotes
|
||||||
|
state = State.readingDoubleQuoted;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(first != commandLine.length) {
|
||||||
|
if(state != State.readingWord && state != State.readingComment)
|
||||||
|
throw new Exception("ran out of data in inappropriate state");
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. $]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
ShellLexeme[] got;
|
||||||
|
|
||||||
|
got = lexShellCommandLine("FOO=bar");
|
||||||
|
assert(got.length == 1);
|
||||||
|
assert(got[0].l == "FOO=bar");
|
||||||
|
|
||||||
|
// comments can only happen at whitespace contexts, not at the end of a single word
|
||||||
|
got = lexShellCommandLine("FOO=bar#commentspam");
|
||||||
|
assert(got.length == 1);
|
||||||
|
assert(got[0].l == "FOO=bar#commentspam");
|
||||||
|
|
||||||
|
got = lexShellCommandLine("FOO=bar #commentspam");
|
||||||
|
assert(got.length == 2);
|
||||||
|
assert(got[0].l == "FOO=bar");
|
||||||
|
assert(got[1].l == " "); // arg separator still there even tho there is no arg cuz of the comment, but that's semantic
|
||||||
|
|
||||||
|
got = lexShellCommandLine("#commentspam");
|
||||||
|
assert(got.length == 0, got[0].l);
|
||||||
|
|
||||||
|
got = lexShellCommandLine("FOO=bar ./prog");
|
||||||
|
assert(got.length == 3);
|
||||||
|
assert(got[0].l == "FOO=bar");
|
||||||
|
assert(got[1].l == " "); // argument separator
|
||||||
|
assert(got[2].l == "./prog");
|
||||||
|
|
||||||
|
// all whitespace should be collapsed to a single argument separator
|
||||||
|
got = lexShellCommandLine("FOO=bar ./prog");
|
||||||
|
assert(got.length == 3);
|
||||||
|
assert(got[0].l == "FOO=bar");
|
||||||
|
assert(got[1].l == " "); // argument separator
|
||||||
|
assert(got[2].l == "./prog");
|
||||||
|
|
||||||
|
got = lexShellCommandLine("'foo'bar");
|
||||||
|
assert(got.length == 2);
|
||||||
|
assert(got[0].l == "foo");
|
||||||
|
assert(got[0].quoteStyle == QuoteStyle.nonExpanding);
|
||||||
|
assert(got[1].l == "bar");
|
||||||
|
assert(got[1].quoteStyle == QuoteStyle.none);
|
||||||
|
|
||||||
|
// escaped single char works as if you wrote it in single quotes
|
||||||
|
got = lexShellCommandLine("test\\'bar");
|
||||||
|
assert(got.length == 3);
|
||||||
|
assert(got[0].l == "test");
|
||||||
|
assert(got[1].l == "'");
|
||||||
|
assert(got[2].l == "bar");
|
||||||
|
|
||||||
|
// checking for utf-8 decode of escaped char
|
||||||
|
got = lexShellCommandLine("test\\\»bar");
|
||||||
|
assert(got.length == 3);
|
||||||
|
assert(got[0].l == "test");
|
||||||
|
assert(got[1].l == "\»");
|
||||||
|
assert(got[2].l == "bar");
|
||||||
|
|
||||||
|
got = lexShellCommandLine(`"ok"`);
|
||||||
|
assert(got.length == 1);
|
||||||
|
assert(got[0].l == "ok");
|
||||||
|
assert(got[0].quoteStyle == QuoteStyle.expanding);
|
||||||
|
|
||||||
|
got = lexShellCommandLine(`"ok\"after"`);
|
||||||
|
assert(got.length == 2);
|
||||||
|
assert(got[0].l == "ok");
|
||||||
|
assert(got[0].quoteStyle == QuoteStyle.expanding);
|
||||||
|
assert(got[1].l == "\"after");
|
||||||
|
assert(got[1].quoteStyle == QuoteStyle.expanding);
|
||||||
|
|
||||||
|
got = lexShellCommandLine(`FOO=bar ./thing 'my ard' second_arg "quoted\"thing"`);
|
||||||
|
assert(got.length == 10); // because quoted\"thing is two in this weird system
|
||||||
|
assert(got[0].l == "FOO=bar");
|
||||||
|
assert(got[1].l == " ");
|
||||||
|
assert(got[2].l == "./thing");
|
||||||
|
assert(got[3].l == " ");
|
||||||
|
assert(got[4].l == "my ard");
|
||||||
|
assert(got[5].l == " ");
|
||||||
|
assert(got[6].l == "second_arg");
|
||||||
|
assert(got[7].l == " ");
|
||||||
|
assert(got[8].l == "quoted");
|
||||||
|
assert(got[9].l == "\"thing");
|
||||||
|
|
||||||
|
got = lexShellCommandLine("a | b c");
|
||||||
|
assert(got.length == 7);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ShellIo {
|
||||||
|
enum Kind {
|
||||||
|
inherit,
|
||||||
|
fd,
|
||||||
|
filename,
|
||||||
|
pipedCommand,
|
||||||
|
memoryBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
Kind kind;
|
||||||
|
int fd;
|
||||||
|
string filename;
|
||||||
|
ShellCommand pipedCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShellCommand {
|
||||||
|
ShellIo stdin;
|
||||||
|
ShellIo stdout;
|
||||||
|
ShellIo stderr;
|
||||||
|
// yes i know in unix you can do other fds too. do i care?
|
||||||
|
|
||||||
|
private ExternalProcess externalProcess;
|
||||||
|
|
||||||
|
string exePath;
|
||||||
|
string cwd;
|
||||||
|
string[] argv;
|
||||||
|
EnvironmentPair[] environmentPairs;
|
||||||
|
|
||||||
|
/++
|
||||||
|
The return value may be null! Some things can be executed without external processes.
|
||||||
|
|
||||||
|
This function is absolutely NOT pure. It may modify your shell context, run external processes, and generally carry out operations outside the shell.
|
||||||
|
+/
|
||||||
|
ExternalProcess execute(ref ShellContext context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
A shell component - which is likely an argument, but that is a semantic distinction we can't make until parsing - may be made up of several lexemes. Think `foo'bar'`. This will extract them from the given array up to and including the next unquoted space or newline char.
|
||||||
|
+/
|
||||||
|
ShellLexeme[] nextComponent(ref ShellLexeme[] lexemes) {
|
||||||
|
if(lexemes.length == 0)
|
||||||
|
return lexemes[$ .. $];
|
||||||
|
|
||||||
|
int pos;
|
||||||
|
while(
|
||||||
|
pos < lexemes.length &&
|
||||||
|
!(
|
||||||
|
// identify an arg or command separator
|
||||||
|
lexemes[pos].quoteStyle == QuoteStyle.none &&
|
||||||
|
(
|
||||||
|
lexemes[pos].l == " " ||
|
||||||
|
lexemes[pos].l == "\n"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pos == 0)
|
||||||
|
pos++; // include the termination condition as its own component
|
||||||
|
|
||||||
|
auto ret = lexemes[0 .. pos];
|
||||||
|
lexemes = lexemes[pos .. $];
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EnvironmentPair {
|
||||||
|
string environmentVariableName;
|
||||||
|
string assignedValue;
|
||||||
|
|
||||||
|
string toString() {
|
||||||
|
return environmentVariableName ~ "=" ~ assignedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string expandSingleArg(string escapedArg, ShellContext context) {
|
||||||
|
return escapedArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Parses a set of lexemes into set of command objects.
|
||||||
|
|
||||||
|
This function in pure in all but formal annotation; it does not interact with the outside world, except through the globber delegate you provide (which should not make any changes to the outside world!).
|
||||||
|
+/
|
||||||
|
ShellCommand[] parseShellCommand(ShellLexeme[] lexemes, ShellContext context, Globber globber) {
|
||||||
|
ShellCommand[] ret;
|
||||||
|
|
||||||
|
ShellCommand currentCommand;
|
||||||
|
ShellCommand firstCommand;
|
||||||
|
|
||||||
|
enum ParseState {
|
||||||
|
lookingForVarAssignment,
|
||||||
|
lookingForArg,
|
||||||
|
}
|
||||||
|
ParseState parseState = ParseState.lookingForVarAssignment;
|
||||||
|
|
||||||
|
while(lexemes.length) {
|
||||||
|
auto component = nextComponent(lexemes);
|
||||||
|
if(component.length) {
|
||||||
|
if(currentCommand is null)
|
||||||
|
currentCommand = new ShellCommand();
|
||||||
|
if(firstCommand is null)
|
||||||
|
firstCommand = currentCommand;
|
||||||
|
|
||||||
|
/+
|
||||||
|
Command syntax in bash is basically:
|
||||||
|
|
||||||
|
Zero or more `ENV=value` sets, separated by whitespace, followed by zero or more arg things.
|
||||||
|
OR
|
||||||
|
a shell builtin which does special things to the rest of the command, and may even require subsequent commands
|
||||||
|
|
||||||
|
Argv[0] can be a shell built in which reads the rest of argv separately. It may even require subsequent commands!
|
||||||
|
|
||||||
|
For some shell built in keywords, you should not actually do expansion:
|
||||||
|
$ for $i in one two; do ls $i; done
|
||||||
|
bash: `$i': not a valid identifier
|
||||||
|
|
||||||
|
So there must be some kind of intermediate representation of possible expansions.
|
||||||
|
|
||||||
|
|
||||||
|
BUT THIS IS MY SHELL I CAN DO WHAT I WANT!!!!!!!!!!!!
|
||||||
|
+/
|
||||||
|
|
||||||
|
bool thisWasEnvironmentPair = false;
|
||||||
|
EnvironmentPair environmentPair;
|
||||||
|
bool thisWasRedirection = false;
|
||||||
|
bool thisWasPipe = false;
|
||||||
|
string arg;
|
||||||
|
|
||||||
|
if(component.length == 0) {
|
||||||
|
// nothing left, should never happen
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(component.length == 1) {
|
||||||
|
if(component[0].quoteStyle == QuoteStyle.none && component[0].l == " ") {
|
||||||
|
// just an arg separator
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(lexeme; component) {
|
||||||
|
again:
|
||||||
|
final switch(parseState) {
|
||||||
|
case ParseState.lookingForVarAssignment:
|
||||||
|
if(thisWasEnvironmentPair) {
|
||||||
|
environmentPair.assignedValue ~= lexeme.toEscapedFormat();
|
||||||
|
} else {
|
||||||
|
// assume there is no var until we prove otherwise
|
||||||
|
parseState = ParseState.lookingForArg;
|
||||||
|
if(lexeme.quoteStyle == QuoteStyle.none) {
|
||||||
|
foreach(idx, ch; lexeme.l) {
|
||||||
|
if(ch == '=') {
|
||||||
|
// actually found one!
|
||||||
|
thisWasEnvironmentPair = true;
|
||||||
|
environmentPair.environmentVariableName = lexeme.l[0 .. idx];
|
||||||
|
environmentPair.assignedValue = lexeme.l[idx + 1 .. $];
|
||||||
|
parseState = ParseState.lookingForVarAssignment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(parseState == ParseState.lookingForArg)
|
||||||
|
goto case;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ParseState.lookingForArg:
|
||||||
|
if(lexeme.quoteStyle == QuoteStyle.none) {
|
||||||
|
if(lexeme.l == "<" || lexeme.l == ">")
|
||||||
|
thisWasRedirection = true;
|
||||||
|
if(lexeme.l == "|")
|
||||||
|
thisWasPipe = true;
|
||||||
|
}
|
||||||
|
arg ~= lexeme.toEscapedFormat();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(thisWasEnvironmentPair) {
|
||||||
|
environmentPair.assignedValue = expandSingleArg(environmentPair.assignedValue, context);
|
||||||
|
currentCommand.environmentPairs ~= environmentPair;
|
||||||
|
} else if(thisWasRedirection) {
|
||||||
|
// FIXME: read the fd off this arg
|
||||||
|
// FIXME: read the filename off the next arg, new parse state
|
||||||
|
assert(0);
|
||||||
|
} else if(thisWasPipe) {
|
||||||
|
// FIXME: read the fd? i kinda wanna support 2| and such
|
||||||
|
auto newCommand = new ShellCommand();
|
||||||
|
currentCommand.stdout.kind = ShellIo.Kind.pipedCommand;
|
||||||
|
currentCommand.stdout.pipedCommand = newCommand;
|
||||||
|
newCommand.stdin.kind = ShellIo.Kind.pipedCommand;
|
||||||
|
newCommand.stdin.pipedCommand = currentCommand;
|
||||||
|
|
||||||
|
currentCommand = newCommand;
|
||||||
|
} else {
|
||||||
|
currentCommand.argv ~= globber(arg, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(firstCommand)
|
||||||
|
ret ~= firstCommand;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
string[] globber(string s, ShellContext context) {
|
||||||
|
return [s];
|
||||||
|
}
|
||||||
|
ShellContext context;
|
||||||
|
ShellCommand[] commands;
|
||||||
|
|
||||||
|
commands = parseShellCommand(lexShellCommandLine("foo bar"), context, &globber);
|
||||||
|
assert(commands.length == 1);
|
||||||
|
assert(commands[0].argv.length == 2);
|
||||||
|
assert(commands[0].argv[0] == "foo");
|
||||||
|
assert(commands[0].argv[1] == "bar");
|
||||||
|
|
||||||
|
commands = parseShellCommand(lexShellCommandLine("foo bar'baz'"), context, &globber);
|
||||||
|
assert(commands.length == 1);
|
||||||
|
assert(commands[0].argv.length == 2);
|
||||||
|
assert(commands[0].argv[0] == "foo");
|
||||||
|
assert(commands[0].argv[1] == "barbaz");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/+
|
||||||
|
interface OSInterface {
|
||||||
|
setEnv
|
||||||
|
getEnv
|
||||||
|
getAllEnv
|
||||||
|
|
||||||
|
runCommand
|
||||||
|
waitForCommand
|
||||||
|
}
|
||||||
|
+/
|
||||||
|
|
||||||
|
class Shell {
|
||||||
|
protected ShellContext context;
|
||||||
|
|
||||||
|
this() {
|
||||||
|
context.cwd = getCurrentWorkingDirectory().toString;
|
||||||
|
prompt = "[deesh]" ~ context.cwd ~ "$ ";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string prompt;
|
||||||
|
|
||||||
|
protected string[] glob(string s) {
|
||||||
|
string[] ret;
|
||||||
|
getFiles(context.cwd, (string name, bool isDirectory) {
|
||||||
|
if(name.length && name[0] == '.' && (s.length == 0 || s[0] != '.'))
|
||||||
|
return; // skip hidden unless specifically requested
|
||||||
|
if(name.matchesFilePattern(s))
|
||||||
|
ret ~= name;
|
||||||
|
});
|
||||||
|
if(ret.length == 0)
|
||||||
|
return [s];
|
||||||
|
else
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final string[] globberForwarder(string s, ShellContext context) {
|
||||||
|
return glob(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dumpCommand(ShellCommand command) {
|
||||||
|
foreach(ep; command.environmentPairs)
|
||||||
|
writeln(ep.toString());
|
||||||
|
writeln(command.argv);
|
||||||
|
if(command.stdout.kind == ShellIo.Kind.pipedCommand) {
|
||||||
|
writeln(" | ");
|
||||||
|
dumpCommand(command.stdout.pipedCommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilePath searchPathForCommand(string arg0) {
|
||||||
|
if(arg0.indexOf("/") != -1)
|
||||||
|
return FilePath(arg0);
|
||||||
|
// could also be built-ins and cmdlets...
|
||||||
|
// and on Windows we should check .exe, .com, .bat, or ask the OS maybe
|
||||||
|
|
||||||
|
version(Posix) { // FIXME
|
||||||
|
immutable searchPaths = ["/usr/bin", "/bin", "/usr/local/bin", "/home/me/bin"]; // FIXME
|
||||||
|
foreach(path; searchPaths) {
|
||||||
|
auto t = FilePath(arg0).makeAbsolute(FilePath(path));
|
||||||
|
import core.sys.posix.sys.stat;
|
||||||
|
stat_t sbuf;
|
||||||
|
|
||||||
|
CharzBuffer buf = t.toString();
|
||||||
|
auto ret = stat(buf.ptr, &sbuf);
|
||||||
|
if(ret != -1)
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FilePath(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
version(Windows)
|
||||||
|
private ExternalProcess startCommand(ShellCommand command, int inheritedPipe, int pgid) {
|
||||||
|
string windowsCommandLine;
|
||||||
|
foreach(arg; command.argv) {
|
||||||
|
if(windowsCommandLine.length)
|
||||||
|
windowsCommandLine ~= " ";
|
||||||
|
windowsCommandLine ~= arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fp = searchPathForCommand(command.argv[0]);
|
||||||
|
|
||||||
|
auto proc = new ExternalProcess(fp, windowsCommandLine);
|
||||||
|
command.externalProcess = proc;
|
||||||
|
proc.start;
|
||||||
|
return proc;
|
||||||
|
}
|
||||||
|
|
||||||
|
version(Posix)
|
||||||
|
private ExternalProcess startCommand(ShellCommand command, int inheritedPipe, int pgid) {
|
||||||
|
auto fp = searchPathForCommand(command.argv[0]);
|
||||||
|
if(fp.isNull()) {
|
||||||
|
throw new Exception("Command not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
import core.sys.posix.unistd;
|
||||||
|
|
||||||
|
int[2] pipes;
|
||||||
|
if(command.stdout.pipedCommand) {
|
||||||
|
auto ret = pipe(pipes);
|
||||||
|
|
||||||
|
setCloExec(pipes[0]);
|
||||||
|
setCloExec(pipes[1]);
|
||||||
|
|
||||||
|
import core.stdc.errno;
|
||||||
|
|
||||||
|
if(ret == -1)
|
||||||
|
throw new ErrnoApiException("stdin pipe", errno);
|
||||||
|
} else {
|
||||||
|
pipes[0] = inheritedPipe;
|
||||||
|
pipes[1] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto proc = new ExternalProcess(fp, command.argv);
|
||||||
|
if(command.stdout.pipedCommand) {
|
||||||
|
proc.beforeExec = () {
|
||||||
|
// reset ignored signals to default behavior
|
||||||
|
import core.sys.posix.signal;
|
||||||
|
signal (SIGINT, SIG_DFL);
|
||||||
|
signal (SIGQUIT, SIG_DFL);
|
||||||
|
signal (SIGTSTP, SIG_DFL);
|
||||||
|
signal (SIGTTIN, SIG_DFL);
|
||||||
|
signal (SIGTTOU, SIG_DFL);
|
||||||
|
signal (SIGCHLD, SIG_DFL);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
proc.pgid = pgid; // 0 here means to lead the group, all subsequent pipe programs should inherit the leader
|
||||||
|
// and inherit the standard handles
|
||||||
|
proc.overrideStdinFd = inheritedPipe;
|
||||||
|
proc.overrideStdoutFd = pipes[1];
|
||||||
|
proc.overrideStderrFd = 2;
|
||||||
|
command.externalProcess = proc;
|
||||||
|
proc.start;
|
||||||
|
|
||||||
|
if(command.stdout.pipedCommand) {
|
||||||
|
startCommand(command.stdout.pipedCommand, pipes[0], pgid ? pgid : proc.pid);
|
||||||
|
|
||||||
|
// we're done with them now
|
||||||
|
close(pipes[0]);
|
||||||
|
close(pipes[1]);
|
||||||
|
pipes[] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return proc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int waitForCommand(ShellCommand command) {
|
||||||
|
command.externalProcess.waitForCompletion();
|
||||||
|
writeln(command.externalProcess.status);
|
||||||
|
if(auto cmd = command.stdout.pipedCommand)
|
||||||
|
waitForCommand(cmd);
|
||||||
|
return command.externalProcess.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execute(string commandLine) {
|
||||||
|
auto commands = parseShellCommand(lexShellCommandLine(commandLine), context, &globberForwarder);
|
||||||
|
foreach(command; commands)
|
||||||
|
try {
|
||||||
|
dumpCommand(command);
|
||||||
|
|
||||||
|
version(Posix) {
|
||||||
|
import core.sys.posix.unistd;
|
||||||
|
import core.sys.posix.signal;
|
||||||
|
|
||||||
|
auto proc = startCommand(command, 0, 0);
|
||||||
|
|
||||||
|
// put the child group in control of the tty
|
||||||
|
ErrnoEnforce!tcsetpgrp(1, proc.pid);
|
||||||
|
kill(-proc.pid, SIGCONT); // and if it beat us to the punch and is waiting, go ahead and wake it up (this is harmless if it is already running
|
||||||
|
waitForCommand(command);
|
||||||
|
// reassert control of the tty to the shell
|
||||||
|
ErrnoEnforce!tcsetpgrp(1, getpid());
|
||||||
|
}
|
||||||
|
|
||||||
|
version(Windows) {
|
||||||
|
auto proc = startCommand(command, 0, 0);
|
||||||
|
waitForCommand(command);
|
||||||
|
}
|
||||||
|
} catch(ArsdExceptionBase e) {
|
||||||
|
string more;
|
||||||
|
e.getAdditionalPrintableInformation((string name, in char[] value) {
|
||||||
|
more ~= ", ";
|
||||||
|
more ~= name ~ ": " ~ value;
|
||||||
|
});
|
||||||
|
writeln("deesh: ", command.argv.length ? command.argv[0] : "", ": ", e.message, more);
|
||||||
|
} catch(Exception e) {
|
||||||
|
writeln("deesh: ", command.argv.length ? command.argv[0] : "", ": ", e.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Constructs an instance of [arsd.terminal.LineGetter] appropriate for use in a repl for this shell.
|
||||||
|
+/
|
||||||
|
auto constructLineGetter()() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/+
|
||||||
|
Parts of bash I like:
|
||||||
|
|
||||||
|
glob expansion
|
||||||
|
! command recall
|
||||||
|
redirection
|
||||||
|
f{1..3} expand to f1 f2 f3. can add ..incr btw
|
||||||
|
f{a,b,c} expand to fa fb fc
|
||||||
|
for i in *; do cmd; done
|
||||||
|
`command expansion`. also $( cmd ) is a thing
|
||||||
|
~ expansion
|
||||||
|
|
||||||
|
foo && bar
|
||||||
|
foo || bar
|
||||||
|
|
||||||
|
$(( maybe arithmetic but idk ))
|
||||||
|
|
||||||
|
ENV=whatever cmd.
|
||||||
|
$ENV ...?
|
||||||
|
|
||||||
|
tab complete!
|
||||||
|
|
||||||
|
PATH lookup. if requested.
|
||||||
|
|
||||||
|
Globbing could insert -- before if there's any - in there.
|
||||||
|
|
||||||
|
Or better yet all the commands must either start with ./
|
||||||
|
or be found internally. Internal can be expanded by definition
|
||||||
|
files that tell how to expand the real thing.
|
||||||
|
|
||||||
|
* flags
|
||||||
|
* arguments
|
||||||
|
* command line
|
||||||
|
* i/o
|
||||||
|
+/
|
||||||
|
|
@ -1164,17 +1164,19 @@ unittest {
|
||||||
|
|
||||||
import arsd.core;
|
import arsd.core;
|
||||||
|
|
||||||
version(D_OpenD) {
|
version(ArsdNoCocoa) {
|
||||||
|
} else {
|
||||||
|
version(D_OpenD) {
|
||||||
version(OSX)
|
version(OSX)
|
||||||
version=OSXCocoa;
|
version=OSXCocoa;
|
||||||
version(iOS)
|
version(iOS)
|
||||||
version=OSXCocoa;
|
version=OSXCocoa;
|
||||||
} else {
|
} else {
|
||||||
version(OSX) version(DigitalMars) version=OSXCocoa;
|
version(OSX) version(DigitalMars) version=OSXCocoa;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
version(Emscripten) {
|
version(Emscripten) {
|
||||||
version=allow_unimplemented_features;
|
version=allow_unimplemented_features;
|
||||||
version=without_opengl;
|
version=without_opengl;
|
||||||
|
|
@ -1416,7 +1418,7 @@ version(Emscripten) {
|
||||||
}
|
}
|
||||||
|
|
||||||
version(libnotify) {
|
version(libnotify) {
|
||||||
pragma(lib, "dl");
|
//pragma(lib, "dl");
|
||||||
import core.sys.posix.dlfcn;
|
import core.sys.posix.dlfcn;
|
||||||
|
|
||||||
void delegate()[int] libnotify_action_delegates;
|
void delegate()[int] libnotify_action_delegates;
|
||||||
|
|
@ -2910,6 +2912,7 @@ class SimpleWindow : CapableOfHandlingNativeEvent, CapableOfBeingDrawnUpon {
|
||||||
else
|
else
|
||||||
XMapWindow(impl.display, impl.window);
|
XMapWindow(impl.display, impl.window);
|
||||||
} else version(OSXCocoa) {
|
} else version(OSXCocoa) {
|
||||||
|
if(impl.window)
|
||||||
impl.window.setIsVisible = !b;
|
impl.window.setIsVisible = !b;
|
||||||
if(!hidden)
|
if(!hidden)
|
||||||
impl.view.setNeedsDisplay(true);
|
impl.view.setNeedsDisplay(true);
|
||||||
|
|
@ -4414,6 +4417,10 @@ struct EventLoopImpl {
|
||||||
|
|
||||||
void delegate(int) signalHandler;
|
void delegate(int) signalHandler;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
version(Posix) {
|
||||||
|
static import unix = core.sys.posix.unistd;
|
||||||
|
}
|
||||||
|
|
||||||
version(X11) {
|
version(X11) {
|
||||||
int pulseFd = -1;
|
int pulseFd = -1;
|
||||||
|
|
@ -4623,7 +4630,7 @@ struct EventLoopImpl {
|
||||||
ref int customEventFDRead() { return SimpleWindow.customEventFDRead; }
|
ref int customEventFDRead() { return SimpleWindow.customEventFDRead; }
|
||||||
version(Posix)
|
version(Posix)
|
||||||
ref int customEventFDWrite() { return SimpleWindow.customEventFDWrite; }
|
ref int customEventFDWrite() { return SimpleWindow.customEventFDWrite; }
|
||||||
version(linux)
|
version(Posix)
|
||||||
ref int customSignalFD() { return SimpleWindow.customSignalFD; }
|
ref int customSignalFD() { return SimpleWindow.customSignalFD; }
|
||||||
version(Windows)
|
version(Windows)
|
||||||
ref auto customEventH() { return SimpleWindow.customEventH; }
|
ref auto customEventH() { return SimpleWindow.customEventH; }
|
||||||
|
|
@ -7355,7 +7362,7 @@ version(Windows) {
|
||||||
}
|
}
|
||||||
|
|
||||||
version (X11) {
|
version (X11) {
|
||||||
pragma(lib, "dl");
|
//pragma(lib, "dl"); // already done by the standard compiler build and specifying it again messes up zig cross compile
|
||||||
import core.sys.posix.dlfcn;
|
import core.sys.posix.dlfcn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -8416,6 +8423,8 @@ struct Pen {
|
||||||
/++
|
/++
|
||||||
Represents an in-memory image in the format that the GUI expects, but with its raw data available to your program.
|
Represents an in-memory image in the format that the GUI expects, but with its raw data available to your program.
|
||||||
|
|
||||||
|
You can load an image with an alpha channel, but you cannot draw that in the current implementation. If you want alpha blending when drawing, use [Sprite] instead.
|
||||||
|
|
||||||
|
|
||||||
On Windows, this means a device-independent bitmap. On X11, it is an XImage.
|
On Windows, this means a device-independent bitmap. On X11, it is an XImage.
|
||||||
|
|
||||||
|
|
@ -9952,7 +9961,14 @@ struct ScreenPainter {
|
||||||
impl.drawPixmap(s, upperLeft.x, upperLeft.y, imageUpperLeft.x, imageUpperLeft.y, sliceSize.width, sliceSize.height);
|
impl.drawPixmap(s, upperLeft.x, upperLeft.y, imageUpperLeft.x, imageUpperLeft.y, sliceSize.width, sliceSize.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/++
|
||||||
|
Draws an [Image] to the window.
|
||||||
|
|
||||||
|
$(WARNING
|
||||||
|
Even if the Image was loaded with `enableAlpha`, drawing may not work!
|
||||||
|
|
||||||
|
Use [Sprite.fromMemoryImage] and [drawPixmap] instead if you want alpha blending to work better.
|
||||||
|
+/
|
||||||
void drawImage(Point upperLeft, Image i, Point upperLeftOfImage = Point(0, 0), int w = 0, int h = 0) {
|
void drawImage(Point upperLeft, Image i, Point upperLeftOfImage = Point(0, 0), int w = 0, int h = 0) {
|
||||||
if(impl is null) return;
|
if(impl is null) return;
|
||||||
//if(isClipped(upperLeft, w, h)) return; // FIXME
|
//if(isClipped(upperLeft, w, h)) return; // FIXME
|
||||||
|
|
@ -9966,6 +9982,8 @@ struct ScreenPainter {
|
||||||
if(upperLeftOfImage.y < 0)
|
if(upperLeftOfImage.y < 0)
|
||||||
upperLeftOfImage.y = 0;
|
upperLeftOfImage.y = 0;
|
||||||
|
|
||||||
|
assert(i.enableAlpha == false, "Alpha blending is not implemented for Image drawing - use Sprite and drawPixmap instead");
|
||||||
|
|
||||||
impl.drawImage(upperLeft.x, upperLeft.y, i, upperLeftOfImage.x, upperLeftOfImage.y, w, h);
|
impl.drawImage(upperLeft.x, upperLeft.y, i, upperLeftOfImage.x, upperLeftOfImage.y, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19453,7 +19471,7 @@ version(OSXCocoa) {
|
||||||
|
|
||||||
auto contentRect = NSRect(NSPoint(0, 0), NSSize(width, height));
|
auto contentRect = NSRect(NSPoint(0, 0), NSSize(width, height));
|
||||||
|
|
||||||
auto window = NSWindow.alloc.initWithContentRect(
|
window = NSWindow.alloc.initWithContentRect(
|
||||||
contentRect,
|
contentRect,
|
||||||
NSWindowStyleMask.resizable | NSWindowStyleMask.closable | NSWindowStyleMask.miniaturizable | NSWindowStyleMask.titled,
|
NSWindowStyleMask.resizable | NSWindowStyleMask.closable | NSWindowStyleMask.miniaturizable | NSWindowStyleMask.titled,
|
||||||
NSBackingStoreType.buffered,
|
NSBackingStoreType.buffered,
|
||||||
|
|
|
||||||
4
sqlite.d
4
sqlite.d
|
|
@ -124,6 +124,10 @@ class Sqlite : Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override bool isAlive() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
override void startTransaction() {
|
override void startTransaction() {
|
||||||
query("BEGIN TRANSACTION");
|
query("BEGIN TRANSACTION");
|
||||||
|
|
|
||||||
73
string.d
73
string.d
|
|
@ -53,3 +53,76 @@ deprecated("D calls this `stripRight` instead") alias trimRight = stripRight;
|
||||||
alias stringz = arsd.core.stringz;
|
alias stringz = arsd.core.stringz;
|
||||||
// CharzBuffer
|
// CharzBuffer
|
||||||
// WCharzBuffer
|
// WCharzBuffer
|
||||||
|
|
||||||
|
// ********* UTILITIES **************
|
||||||
|
|
||||||
|
string[] split(string s, string onWhat) {
|
||||||
|
assert(onWhat.length);
|
||||||
|
string[] ret;
|
||||||
|
more:
|
||||||
|
auto idx = s.indexOf(onWhat);
|
||||||
|
if(idx == -1) {
|
||||||
|
ret ~= s;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
ret ~= s[0 .. idx];
|
||||||
|
s = s[idx + onWhat.length .. $];
|
||||||
|
goto more;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
assert("foo.bar".split(".") == ["foo", "bar"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ptrdiff_t lastIndexOf(string s, string what) {
|
||||||
|
assert(what.length);
|
||||||
|
if(s.length < what.length)
|
||||||
|
return -1;
|
||||||
|
ptrdiff_t checking = s.length - what.length;
|
||||||
|
while(checking >= 0) {
|
||||||
|
if(s[checking .. checking + what.length] == what)
|
||||||
|
return checking;
|
||||||
|
|
||||||
|
checking--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
assert("31234".lastIndexOf("3") == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
string join(string[] str, string w) {
|
||||||
|
string ret;
|
||||||
|
foreach(i, s; str) {
|
||||||
|
if(i)
|
||||||
|
ret ~= w;
|
||||||
|
ret ~= s;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
assert(["a", "b"].join(" ") == "a b");
|
||||||
|
}
|
||||||
|
|
||||||
|
string replace(string str, string find, string repacement) {
|
||||||
|
assert(find.length);
|
||||||
|
|
||||||
|
string ret;
|
||||||
|
more:
|
||||||
|
auto idx = str.indexOf(find);
|
||||||
|
if(idx == -1) {
|
||||||
|
ret ~= str;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
ret ~= str[0 .. idx];
|
||||||
|
ret ~= repacement;
|
||||||
|
str = str[idx + find.length .. $];
|
||||||
|
goto more;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
assert("foobarfoo".replace("foo", "bar") == "barbarbar");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9268,6 +9268,9 @@ version(TerminalDirectToEmulator) {
|
||||||
|
|
||||||
private class TerminalEmulatorInsideWidget : TerminalEmulator {
|
private class TerminalEmulatorInsideWidget : TerminalEmulator {
|
||||||
|
|
||||||
|
import arsd.core : EnableSynchronization;
|
||||||
|
mixin EnableSynchronization;
|
||||||
|
|
||||||
private ScrollbackBuffer sbb() { return scrollbackBuffer; }
|
private ScrollbackBuffer sbb() { return scrollbackBuffer; }
|
||||||
|
|
||||||
void resized(int w, int h) {
|
void resized(int w, int h) {
|
||||||
|
|
@ -9467,8 +9470,8 @@ version(TerminalDirectToEmulator) {
|
||||||
if(this.font.isNull) {
|
if(this.font.isNull) {
|
||||||
// carry on, it will try a default later
|
// carry on, it will try a default later
|
||||||
} else if(this.font.isMonospace) {
|
} else if(this.font.isMonospace) {
|
||||||
this.fontWidth = font.averageWidth;
|
this.fontWidth = castFnumToCnum(font.averageWidth);
|
||||||
this.fontHeight = font.height;
|
this.fontHeight = castFnumToCnum(font.height);
|
||||||
} else {
|
} else {
|
||||||
this.font.unload(); // can't really use a non-monospace font, so just going to unload it so the default font loads again
|
this.font.unload(); // can't really use a non-monospace font, so just going to unload it so the default font loads again
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,13 @@ struct ScopeBuffer(T, size_t maxSize, bool allowGrowth = false) {
|
||||||
size_t length;
|
size_t length;
|
||||||
bool isNull = true;
|
bool isNull = true;
|
||||||
T[] opSlice() { return isNull ? null : buffer[0 .. length]; }
|
T[] opSlice() { return isNull ? null : buffer[0 .. length]; }
|
||||||
|
|
||||||
|
static if(is(T == char))
|
||||||
|
void appendIntAsString(int n) {
|
||||||
|
import std.conv;
|
||||||
|
this ~= to!string(n);
|
||||||
|
}
|
||||||
|
|
||||||
void opOpAssign(string op : "~")(in T rhs) {
|
void opOpAssign(string op : "~")(in T rhs) {
|
||||||
if(buffer is null) buffer = bufferInternal[];
|
if(buffer is null) buffer = bufferInternal[];
|
||||||
isNull = false;
|
isNull = false;
|
||||||
|
|
@ -327,6 +334,7 @@ class TerminalEmulator {
|
||||||
if(termY >= screenHeight)
|
if(termY >= screenHeight)
|
||||||
termY = screenHeight - 1;
|
termY = screenHeight - 1;
|
||||||
|
|
||||||
|
/+
|
||||||
version(Windows) {
|
version(Windows) {
|
||||||
// I'm swapping these because my laptop doesn't have a middle button,
|
// I'm swapping these because my laptop doesn't have a middle button,
|
||||||
// and putty swaps them too by default so whatevs.
|
// and putty swaps them too by default so whatevs.
|
||||||
|
|
@ -335,6 +343,7 @@ class TerminalEmulator {
|
||||||
else if(button == MouseButton.middle)
|
else if(button == MouseButton.middle)
|
||||||
button = MouseButton.right;
|
button = MouseButton.right;
|
||||||
}
|
}
|
||||||
|
+/
|
||||||
|
|
||||||
int baseEventCode() {
|
int baseEventCode() {
|
||||||
int b;
|
int b;
|
||||||
|
|
@ -360,6 +369,9 @@ class TerminalEmulator {
|
||||||
if(alt) // sending alt as meta
|
if(alt) // sending alt as meta
|
||||||
b |= 8;
|
b |= 8;
|
||||||
|
|
||||||
|
if(!sgrMouseMode)
|
||||||
|
b |= 32; // it just always does this
|
||||||
|
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,14 +423,9 @@ class TerminalEmulator {
|
||||||
dragging = false;
|
dragging = false;
|
||||||
if(mouseButtonReleaseTracking) {
|
if(mouseButtonReleaseTracking) {
|
||||||
int b = baseEventCode;
|
int b = baseEventCode;
|
||||||
|
if(!sgrMouseMode)
|
||||||
b |= 3; // always send none / button released
|
b |= 3; // always send none / button released
|
||||||
ScopeBuffer!(char, 16) buffer;
|
sendMouseEvent(b, termX, termY, true);
|
||||||
buffer ~= "\033[M";
|
|
||||||
buffer ~= cast(char) (b | 32);
|
|
||||||
addMouseCoordinates(buffer, termX, termY);
|
|
||||||
//buffer ~= cast(char) (termX+1 + 32);
|
|
||||||
//buffer ~= cast(char) (termY+1 + 32);
|
|
||||||
sendToApplication(buffer[]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -428,13 +435,7 @@ class TerminalEmulator {
|
||||||
lastDragX = termX;
|
lastDragX = termX;
|
||||||
if(mouseMotionTracking || (mouseButtonMotionTracking && button)) {
|
if(mouseMotionTracking || (mouseButtonMotionTracking && button)) {
|
||||||
int b = baseEventCode;
|
int b = baseEventCode;
|
||||||
ScopeBuffer!(char, 16) buffer;
|
sendMouseEvent(b + 32, termX, termY);
|
||||||
buffer ~= "\033[M";
|
|
||||||
buffer ~= cast(char) ((b | 32) + 32);
|
|
||||||
addMouseCoordinates(buffer, termX, termY);
|
|
||||||
//buffer ~= cast(char) (termX+1 + 32);
|
|
||||||
//buffer ~= cast(char) (termY+1 + 32);
|
|
||||||
sendToApplication(buffer[]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(dragging) {
|
if(dragging) {
|
||||||
|
|
@ -511,18 +512,9 @@ class TerminalEmulator {
|
||||||
|
|
||||||
int b = baseEventCode;
|
int b = baseEventCode;
|
||||||
|
|
||||||
int x = termX;
|
sendMouseEvent(b, termX, termY);
|
||||||
int y = termY;
|
|
||||||
x++; y++; // applications expect it to be one-based
|
|
||||||
|
|
||||||
ScopeBuffer!(char, 16) buffer;
|
|
||||||
buffer ~= "\033[M";
|
|
||||||
buffer ~= cast(char) (b | 32);
|
|
||||||
addMouseCoordinates(buffer, termX, termY);
|
|
||||||
//buffer ~= cast(char) (x + 32);
|
//buffer ~= cast(char) (x + 32);
|
||||||
//buffer ~= cast(char) (y + 32);
|
//buffer ~= cast(char) (y + 32);
|
||||||
|
|
||||||
sendToApplication(buffer[]);
|
|
||||||
} else {
|
} else {
|
||||||
do_default_behavior:
|
do_default_behavior:
|
||||||
if(button == MouseButton.middle) {
|
if(button == MouseButton.middle) {
|
||||||
|
|
@ -623,7 +615,22 @@ class TerminalEmulator {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addMouseCoordinates(ref ScopeBuffer!(char, 16) buffer, int x, int y) {
|
private void sendMouseEvent(int b, int x, int y, bool isRelease = false) {
|
||||||
|
|
||||||
|
ScopeBuffer!(char, 16) buffer;
|
||||||
|
|
||||||
|
if(sgrMouseMode) {
|
||||||
|
buffer ~= "\033[<";
|
||||||
|
buffer.appendIntAsString(b);
|
||||||
|
buffer ~= ";";
|
||||||
|
buffer.appendIntAsString(x + 1);
|
||||||
|
buffer ~= ";";
|
||||||
|
buffer.appendIntAsString(y + 1);
|
||||||
|
buffer ~= isRelease ? "m" : "M";
|
||||||
|
} else {
|
||||||
|
buffer ~= "\033[M";
|
||||||
|
buffer ~= cast(char) b;
|
||||||
|
|
||||||
// 1-based stuff and 32 is the base value
|
// 1-based stuff and 32 is the base value
|
||||||
x += 1 + 32;
|
x += 1 + 32;
|
||||||
y += 1 + 32;
|
y += 1 + 32;
|
||||||
|
|
@ -643,6 +650,9 @@ class TerminalEmulator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendToApplication(buffer[]);
|
||||||
|
}
|
||||||
|
|
||||||
protected void returnToNormalScreen() {
|
protected void returnToNormalScreen() {
|
||||||
alternateScreenActive = false;
|
alternateScreenActive = false;
|
||||||
|
|
||||||
|
|
@ -1983,6 +1993,7 @@ class TerminalEmulator {
|
||||||
bool mouseButtonTracking;
|
bool mouseButtonTracking;
|
||||||
private bool _mouseMotionTracking;
|
private bool _mouseMotionTracking;
|
||||||
bool utf8MouseMode;
|
bool utf8MouseMode;
|
||||||
|
bool sgrMouseMode;
|
||||||
bool mouseButtonReleaseTracking;
|
bool mouseButtonReleaseTracking;
|
||||||
bool mouseButtonMotionTracking;
|
bool mouseButtonMotionTracking;
|
||||||
bool selectiveMouseTracking;
|
bool selectiveMouseTracking;
|
||||||
|
|
@ -3223,7 +3234,10 @@ SGR (1006)
|
||||||
The highlight tracking responses are also modified to an SGR-
|
The highlight tracking responses are also modified to an SGR-
|
||||||
like format, using the same SGR-style scheme and button-encod-
|
like format, using the same SGR-style scheme and button-encod-
|
||||||
ings.
|
ings.
|
||||||
|
|
||||||
|
Note that M is used for motion; m is only release
|
||||||
*/
|
*/
|
||||||
|
sgrMouseMode = true;
|
||||||
break;
|
break;
|
||||||
case 1014:
|
case 1014:
|
||||||
// ARSD extension: it is 1002 but selective, only
|
// ARSD extension: it is 1002 but selective, only
|
||||||
|
|
@ -3300,6 +3314,7 @@ URXVT (1015)
|
||||||
break;
|
break;
|
||||||
case 1006:
|
case 1006:
|
||||||
// turn off sgr mouse
|
// turn off sgr mouse
|
||||||
|
sgrMouseMode = false;
|
||||||
break;
|
break;
|
||||||
case 1015:
|
case 1015:
|
||||||
// turn off urxvt mouse
|
// turn off urxvt mouse
|
||||||
|
|
|
||||||
|
|
@ -1503,6 +1503,10 @@ class TextLayouter {
|
||||||
|
|
||||||
auto bb = boundingBoxOfGlyph(segmentIndex, selection.focus);
|
auto bb = boundingBoxOfGlyph(segmentIndex, selection.focus);
|
||||||
|
|
||||||
|
// the y is added elsewhere already. i think.
|
||||||
|
bb.left += segment.upperLeft.x;
|
||||||
|
bb.right += segment.upperLeft.x;
|
||||||
|
|
||||||
bb.top += castFnumToCnum(glyphStyle.font.ascent);
|
bb.top += castFnumToCnum(glyphStyle.font.ascent);
|
||||||
bb.bottom -= castFnumToCnum(glyphStyle.font.descent);
|
bb.bottom -= castFnumToCnum(glyphStyle.font.descent);
|
||||||
|
|
||||||
|
|
|
||||||
628
uri.d
628
uri.d
|
|
@ -8,6 +8,9 @@ module arsd.uri;
|
||||||
|
|
||||||
import arsd.core;
|
import arsd.core;
|
||||||
|
|
||||||
|
import arsd.conv;
|
||||||
|
import arsd.string;
|
||||||
|
|
||||||
alias encodeUriComponent = arsd.core.encodeUriComponent;
|
alias encodeUriComponent = arsd.core.encodeUriComponent;
|
||||||
alias decodeUriComponent = arsd.core.decodeUriComponent;
|
alias decodeUriComponent = arsd.core.decodeUriComponent;
|
||||||
|
|
||||||
|
|
@ -18,3 +21,628 @@ alias decodeComponent = decodeUriComponent;
|
||||||
// FIXME: merge and pull Uri struct from http2 and cgi. maybe via core.
|
// FIXME: merge and pull Uri struct from http2 and cgi. maybe via core.
|
||||||
|
|
||||||
// might also put base64 in here....
|
// might also put base64 in here....
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/++
|
||||||
|
Represents a URI. It offers named access to the components and relative uri resolution, though as a user of the library, you'd mostly just construct it like `Uri("http://example.com/index.html")`.
|
||||||
|
|
||||||
|
History:
|
||||||
|
Moved from duplication in [arsd.cgi] and [arsd.http2] to arsd.uri on November 2, 2025.
|
||||||
|
+/
|
||||||
|
struct Uri {
|
||||||
|
UriString toUriString() {
|
||||||
|
return UriString(toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
alias toUriString this; // blargh idk a url really is a string, but should it be implicit?
|
||||||
|
|
||||||
|
// scheme://userinfo@host:port/path?query#fragment
|
||||||
|
|
||||||
|
string scheme; /// e.g. "http" in "http://example.com/"
|
||||||
|
string userinfo; /// the username (and possibly a password) in the uri
|
||||||
|
string host; /// the domain name. note it may be an ip address or have percent encoding too.
|
||||||
|
int port; /// port number, if given. Will be zero if a port was not explicitly given
|
||||||
|
string path; /// e.g. "/folder/file.html" in "http://example.com/folder/file.html"
|
||||||
|
string query; /// the stuff after the ? in a uri
|
||||||
|
string fragment; /// the stuff after the # in a uri.
|
||||||
|
|
||||||
|
// cgi.d specific.......
|
||||||
|
// idk if i want to keep these, since the functions they wrap are used many, many, many times in existing code, so this is either an unnecessary alias or a gratuitous break of compatibility
|
||||||
|
// the decode ones need to keep different names anyway because we can't overload on return values...
|
||||||
|
static string encode(string s) { return encodeUriComponent(s); }
|
||||||
|
static string encode(string[string] s) { return encodeVariables(s); }
|
||||||
|
static string encode(string[][string] s) { return encodeVariables(s); }
|
||||||
|
|
||||||
|
/++
|
||||||
|
Parses an existing uri string (which should be pre-validated) into this further detailed structure.
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added November 2, 2025.
|
||||||
|
+/
|
||||||
|
this(UriString uriString) {
|
||||||
|
this(uriString.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Transforms an interpolated expression sequence into a uri, encoding as appropriate as it reads.
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added November 2, 2025.
|
||||||
|
+/
|
||||||
|
this(Args...)(InterpolationHeader header, Args args, InterpolationFooter footer) {
|
||||||
|
// will need to use iraw here for some cases. paths may partially encoded but still allow slashes, prolly needs a type.
|
||||||
|
// so like $(path(x)) or $(queryString(x)) or maybe isemi or something. or make user split it into a string[] then recombine here....
|
||||||
|
string thing;
|
||||||
|
foreach(arg; args) {
|
||||||
|
static if(is(typeof(arg) == InterpolationHeader))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) == InterpolationFooter))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) == InterpolatedLiteral!part, string part))
|
||||||
|
thing ~= part;
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) == InterpolatedExpression!code, string code))
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
static if(is(typeof(arg) == iraw))
|
||||||
|
thing ~= iraw.s;
|
||||||
|
else
|
||||||
|
thing ~= encodeUriComponent(to!string(arg));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this(thing);
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
string bar = "12/";
|
||||||
|
string baz = "&omg";
|
||||||
|
auto uri = Uri(i"http://example.com/foo/$bar?thing=$baz");
|
||||||
|
|
||||||
|
assert(uri.toString() == "http://example.com/foo/12%2F?thing=%26omg");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Breaks down a uri string to its components
|
||||||
|
this(string uri) {
|
||||||
|
size_t lastGoodIndex;
|
||||||
|
foreach(char ch; uri) {
|
||||||
|
if(ch > 127) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lastGoodIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
string replacement = uri[0 .. lastGoodIndex];
|
||||||
|
foreach(char ch; uri[lastGoodIndex .. $]) {
|
||||||
|
if(ch > 127) {
|
||||||
|
// need to percent-encode any non-ascii in it
|
||||||
|
char[3] buffer;
|
||||||
|
buffer[0] = '%';
|
||||||
|
|
||||||
|
auto first = ch / 16;
|
||||||
|
auto second = ch % 16;
|
||||||
|
first += (first >= 10) ? ('A'-10) : '0';
|
||||||
|
second += (second >= 10) ? ('A'-10) : '0';
|
||||||
|
|
||||||
|
buffer[1] = cast(char) first;
|
||||||
|
buffer[2] = cast(char) second;
|
||||||
|
|
||||||
|
replacement ~= buffer[];
|
||||||
|
} else {
|
||||||
|
replacement ~= ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reparse(replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `port` if set, otherwise if scheme is https 443, otherwise always 80
|
||||||
|
int effectivePort() const @property nothrow pure @safe @nogc {
|
||||||
|
return port != 0 ? port
|
||||||
|
: scheme == "https" ? 443 : 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
package string unixSocketPath = null;
|
||||||
|
/// Indicates it should be accessed through a unix socket instead of regular tcp. Returns new version without modifying this object.
|
||||||
|
Uri viaUnixSocket(string path) const {
|
||||||
|
Uri copy = this;
|
||||||
|
copy.unixSocketPath = path;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Goes through a unix socket in the abstract namespace (linux only). Returns new version without modifying this object.
|
||||||
|
version(linux)
|
||||||
|
Uri viaAbstractSocket(string path) const {
|
||||||
|
Uri copy = this;
|
||||||
|
copy.unixSocketPath = "\0" ~ path;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// these are like javascript's location.search and location.hash
|
||||||
|
string search() const {
|
||||||
|
return query.length ? ("?" ~ query) : "";
|
||||||
|
}
|
||||||
|
string hash() const {
|
||||||
|
return fragment.length ? ("#" ~ fragment) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void reparse(string uri) {
|
||||||
|
// from RFC 3986
|
||||||
|
// the ctRegex triples the compile time and makes ugly errors for no real benefit
|
||||||
|
// it was a nice experiment but just not worth it.
|
||||||
|
// enum ctr = ctRegex!r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?";
|
||||||
|
/*
|
||||||
|
Captures:
|
||||||
|
0 = whole url
|
||||||
|
1 = scheme, with :
|
||||||
|
2 = scheme, no :
|
||||||
|
3 = authority, with //
|
||||||
|
4 = authority, no //
|
||||||
|
5 = path
|
||||||
|
6 = query string, with ?
|
||||||
|
7 = query string, no ?
|
||||||
|
8 = anchor, with #
|
||||||
|
9 = anchor, no #
|
||||||
|
*/
|
||||||
|
// Yikes, even regular, non-CT regex is also unacceptably slow to compile. 1.9s on my computer!
|
||||||
|
// instead, I will DIY and cut that down to 0.6s on the same computer.
|
||||||
|
/*
|
||||||
|
|
||||||
|
Note that authority is
|
||||||
|
user:password@domain:port
|
||||||
|
where the user:password@ part is optional, and the :port is optional.
|
||||||
|
|
||||||
|
Regex translation:
|
||||||
|
|
||||||
|
Scheme cannot have :, /, ?, or # in it, and must have one or more chars and end in a :. It is optional, but must be first.
|
||||||
|
Authority must start with //, but cannot have any other /, ?, or # in it. It is optional.
|
||||||
|
Path cannot have any ? or # in it. It is optional.
|
||||||
|
Query must start with ? and must not have # in it. It is optional.
|
||||||
|
Anchor must start with # and can have anything else in it to end of string. It is optional.
|
||||||
|
*/
|
||||||
|
|
||||||
|
this = Uri.init; // reset all state
|
||||||
|
|
||||||
|
// empty uri = nothing special
|
||||||
|
if(uri.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t idx;
|
||||||
|
|
||||||
|
scheme_loop: foreach(char c; uri[idx .. $]) {
|
||||||
|
switch(c) {
|
||||||
|
case ':':
|
||||||
|
case '/':
|
||||||
|
case '?':
|
||||||
|
case '#':
|
||||||
|
break scheme_loop;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(idx == 0 && uri[idx] == ':') {
|
||||||
|
// this is actually a path! we skip way ahead
|
||||||
|
goto path_loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(idx == uri.length) {
|
||||||
|
// the whole thing is a path, apparently
|
||||||
|
path = uri;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(idx > 0 && uri[idx] == ':') {
|
||||||
|
scheme = uri[0 .. idx];
|
||||||
|
idx++;
|
||||||
|
} else {
|
||||||
|
// we need to rewind; it found a / but no :, so the whole thing is prolly a path...
|
||||||
|
idx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(idx + 2 < uri.length && uri[idx .. idx + 2] == "//") {
|
||||||
|
// we have an authority....
|
||||||
|
idx += 2;
|
||||||
|
|
||||||
|
auto authority_start = idx;
|
||||||
|
authority_loop: foreach(char c; uri[idx .. $]) {
|
||||||
|
switch(c) {
|
||||||
|
case '/':
|
||||||
|
case '?':
|
||||||
|
case '#':
|
||||||
|
break authority_loop;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto authority = uri[authority_start .. idx];
|
||||||
|
|
||||||
|
auto idx2 = authority.indexOf("@");
|
||||||
|
if(idx2 != -1) {
|
||||||
|
userinfo = authority[0 .. idx2];
|
||||||
|
authority = authority[idx2 + 1 .. $];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(authority.length && authority[0] == '[') {
|
||||||
|
// ipv6 address special casing
|
||||||
|
idx2 = authority.indexOf("]");
|
||||||
|
if(idx2 != -1) {
|
||||||
|
auto end = authority[idx2 + 1 .. $];
|
||||||
|
if(end.length && end[0] == ':')
|
||||||
|
idx2 = idx2 + 1;
|
||||||
|
else
|
||||||
|
idx2 = -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
idx2 = authority.indexOf(":");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(idx2 == -1) {
|
||||||
|
port = 0; // 0 means not specified; we should use the default for the scheme
|
||||||
|
host = authority;
|
||||||
|
} else {
|
||||||
|
host = authority[0 .. idx2];
|
||||||
|
if(idx2 + 1 < authority.length)
|
||||||
|
port = to!int(authority[idx2 + 1 .. $]);
|
||||||
|
else
|
||||||
|
port = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path_loop:
|
||||||
|
auto path_start = idx;
|
||||||
|
|
||||||
|
foreach(char c; uri[idx .. $]) {
|
||||||
|
if(c == '?' || c == '#')
|
||||||
|
break;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
path = uri[path_start .. idx];
|
||||||
|
|
||||||
|
if(idx == uri.length)
|
||||||
|
return; // nothing more to examine...
|
||||||
|
|
||||||
|
if(uri[idx] == '?') {
|
||||||
|
idx++;
|
||||||
|
auto query_start = idx;
|
||||||
|
foreach(char c; uri[idx .. $]) {
|
||||||
|
if(c == '#')
|
||||||
|
break;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
query = uri[query_start .. idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(idx < uri.length && uri[idx] == '#') {
|
||||||
|
idx++;
|
||||||
|
fragment = uri[idx .. $];
|
||||||
|
}
|
||||||
|
|
||||||
|
// uriInvalidated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string rebuildUri() const {
|
||||||
|
string ret;
|
||||||
|
if(scheme.length)
|
||||||
|
ret ~= scheme ~ ":";
|
||||||
|
if(userinfo.length || host.length)
|
||||||
|
ret ~= "//";
|
||||||
|
if(userinfo.length)
|
||||||
|
ret ~= userinfo ~ "@";
|
||||||
|
if(host.length)
|
||||||
|
ret ~= host;
|
||||||
|
if(port)
|
||||||
|
ret ~= ":" ~ to!string(port);
|
||||||
|
|
||||||
|
ret ~= path;
|
||||||
|
|
||||||
|
if(query.length)
|
||||||
|
ret ~= "?" ~ query;
|
||||||
|
|
||||||
|
if(fragment.length)
|
||||||
|
ret ~= "#" ~ fragment;
|
||||||
|
|
||||||
|
// uri = ret;
|
||||||
|
// uriInvalidated = false;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the broken down parts back into a complete string
|
||||||
|
string toString() const {
|
||||||
|
// if(uriInvalidated)
|
||||||
|
return rebuildUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new absolute Uri given a base. It treats this one as
|
||||||
|
/// relative where possible, but absolute if not. (If protocol, domain, or
|
||||||
|
/// other info is not set, the new one inherits it from the base.)
|
||||||
|
///
|
||||||
|
/// Browsers use a function like this to figure out links in html.
|
||||||
|
Uri basedOn(in Uri baseUrl) const {
|
||||||
|
Uri n = this; // copies
|
||||||
|
if(n.scheme == "data")
|
||||||
|
return n;
|
||||||
|
// n.uriInvalidated = true; // make sure we regenerate...
|
||||||
|
|
||||||
|
// userinfo is not inherited... is this wrong?
|
||||||
|
|
||||||
|
// if anything is given in the existing url, we don't use the base anymore.
|
||||||
|
if(n.scheme.length == 0) {
|
||||||
|
n.scheme = baseUrl.scheme;
|
||||||
|
if(n.host.length == 0) {
|
||||||
|
n.host = baseUrl.host;
|
||||||
|
if(n.port == 0) {
|
||||||
|
n.port = baseUrl.port;
|
||||||
|
if(n.path.length > 0 && n.path[0] != '/') {
|
||||||
|
auto b = baseUrl.path[0 .. baseUrl.path.lastIndexOf("/") + 1];
|
||||||
|
if(b.length == 0)
|
||||||
|
b = "/";
|
||||||
|
n.path = b ~ n.path;
|
||||||
|
} else if(n.path.length == 0) {
|
||||||
|
n.path = baseUrl.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n.removeDots();
|
||||||
|
|
||||||
|
// if still basically talking to the same thing, we should inherit the unix path
|
||||||
|
// too since basically the unix path is saying for this service, always use this override.
|
||||||
|
if(n.host == baseUrl.host && n.scheme == baseUrl.scheme && n.port == baseUrl.port)
|
||||||
|
n.unixSocketPath = baseUrl.unixSocketPath;
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Resolves ../ and ./ parts of the path. Used in the implementation of [basedOn] and you could also use it to normalize things.
|
||||||
|
+/
|
||||||
|
void removeDots() {
|
||||||
|
auto parts = this.path.split("/");
|
||||||
|
string[] toKeep;
|
||||||
|
foreach(part; parts) {
|
||||||
|
if(part == ".") {
|
||||||
|
continue;
|
||||||
|
} else if(part == "..") {
|
||||||
|
//if(toKeep.length > 1)
|
||||||
|
toKeep = toKeep[0 .. $-1];
|
||||||
|
//else
|
||||||
|
//toKeep = [""];
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
//if(toKeep.length && toKeep[$-1].length == 0 && part.length == 0)
|
||||||
|
//continue; // skip a `//` situation
|
||||||
|
toKeep ~= part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto path = toKeep.join("/");
|
||||||
|
if(path.length && path[0] != '/')
|
||||||
|
path = "/" ~ path;
|
||||||
|
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
auto uri = Uri("test.html");
|
||||||
|
assert(uri.path == "test.html");
|
||||||
|
uri = Uri("path/1/lol");
|
||||||
|
assert(uri.path == "path/1/lol");
|
||||||
|
uri = Uri("http://me@example.com");
|
||||||
|
assert(uri.scheme == "http");
|
||||||
|
assert(uri.userinfo == "me");
|
||||||
|
assert(uri.host == "example.com");
|
||||||
|
uri = Uri("http://example.com/#a");
|
||||||
|
assert(uri.scheme == "http");
|
||||||
|
assert(uri.host == "example.com");
|
||||||
|
assert(uri.fragment == "a");
|
||||||
|
uri = Uri("#foo");
|
||||||
|
assert(uri.fragment == "foo");
|
||||||
|
uri = Uri("?lol");
|
||||||
|
assert(uri.query == "lol");
|
||||||
|
uri = Uri("#foo?lol");
|
||||||
|
assert(uri.fragment == "foo?lol");
|
||||||
|
uri = Uri("?lol#foo");
|
||||||
|
assert(uri.fragment == "foo");
|
||||||
|
assert(uri.query == "lol");
|
||||||
|
|
||||||
|
uri = Uri("http://127.0.0.1/");
|
||||||
|
assert(uri.host == "127.0.0.1");
|
||||||
|
assert(uri.port == 0);
|
||||||
|
|
||||||
|
uri = Uri("http://127.0.0.1:123/");
|
||||||
|
assert(uri.host == "127.0.0.1");
|
||||||
|
assert(uri.port == 123);
|
||||||
|
|
||||||
|
uri = Uri("http://[ff:ff::0]/");
|
||||||
|
assert(uri.host == "[ff:ff::0]");
|
||||||
|
|
||||||
|
uri = Uri("http://[ff:ff::0]:123/");
|
||||||
|
assert(uri.host == "[ff:ff::0]");
|
||||||
|
assert(uri.port == 123);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This can sometimes be a big pain in the butt for me, so lots of copy/paste here to cover
|
||||||
|
// the possibilities.
|
||||||
|
unittest {
|
||||||
|
auto url = Uri("cool.html"); // checking relative links
|
||||||
|
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/cool.html");
|
||||||
|
assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html");
|
||||||
|
|
||||||
|
url = Uri("/something/cool.html"); // same server, different path
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/something/cool.html");
|
||||||
|
assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/something/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/something/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/something/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/something/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/something/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/something/cool.html");
|
||||||
|
assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html");
|
||||||
|
|
||||||
|
url = Uri("?query=answer"); // same path. server, protocol, and port, just different query string and fragment
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/test.html?query=answer");
|
||||||
|
assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/test.html?query=answer");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/?query=answer");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/?query=answer");
|
||||||
|
assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/test.html?query=answer");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/test.html?query=answer");
|
||||||
|
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/test.html?query=answer");
|
||||||
|
assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer");
|
||||||
|
|
||||||
|
url = Uri("/test/bar");
|
||||||
|
assert(Uri("./").basedOn(url) == "/test/", Uri("./").basedOn(url));
|
||||||
|
assert(Uri("../").basedOn(url) == "/");
|
||||||
|
|
||||||
|
url = Uri("http://example.com/");
|
||||||
|
assert(Uri("../foo").basedOn(url) == "http://example.com/foo");
|
||||||
|
|
||||||
|
//auto uriBefore = url;
|
||||||
|
url = Uri("#anchor"); // everything should remain the same except the anchor
|
||||||
|
//uriBefore.anchor = "anchor");
|
||||||
|
//assert(url == uriBefore);
|
||||||
|
|
||||||
|
url = Uri("//example.com"); // same protocol, but different server. the path here should be blank.
|
||||||
|
|
||||||
|
url = Uri("//example.com/example.html"); // same protocol, but different server and path
|
||||||
|
|
||||||
|
url = Uri("http://example.com/test.html"); // completely absolute link should never be modified
|
||||||
|
|
||||||
|
url = Uri("http://example.com"); // completely absolute link should never be modified, even if it has no path
|
||||||
|
|
||||||
|
// FIXME: add something for port too
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes a data:// uri that can be used as links in most newer browsers (IE8+).
|
||||||
|
string makeDataUrl()(string mimeType, in void[] data) {
|
||||||
|
import std.base64; // FIXME then i can remove the () template
|
||||||
|
auto data64 = Base64.encode(cast(const(ubyte[])) data);
|
||||||
|
return "data:" ~ mimeType ~ ";base64," ~ cast(string)(data64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// breaks down a url encoded string
|
||||||
|
string[][string] decodeVariables(string data, string separator = "&", string[]* namesInOrder = null, string[]* valuesInOrder = null) {
|
||||||
|
auto vars = data.split(separator);
|
||||||
|
string[][string] _get;
|
||||||
|
foreach(var; vars) {
|
||||||
|
auto equal = var.indexOf("=");
|
||||||
|
string name;
|
||||||
|
string value;
|
||||||
|
if(equal == -1) {
|
||||||
|
name = decodeUriComponent(var);
|
||||||
|
value = "";
|
||||||
|
} else {
|
||||||
|
//_get[decodeUriComponent(var[0..equal])] ~= decodeUriComponent(var[equal + 1 .. $].replace("+", " "));
|
||||||
|
// stupid + -> space conversion.
|
||||||
|
name = decodeUriComponent(var[0..equal].replace("+", " "));
|
||||||
|
value = decodeUriComponent(var[equal + 1 .. $].replace("+", " "));
|
||||||
|
}
|
||||||
|
|
||||||
|
_get[name] ~= value;
|
||||||
|
if(namesInOrder)
|
||||||
|
(*namesInOrder) ~= name;
|
||||||
|
if(valuesInOrder)
|
||||||
|
(*valuesInOrder) ~= value;
|
||||||
|
}
|
||||||
|
return _get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// breaks down a url encoded string, but only returns the last value of any array
|
||||||
|
string[string] decodeVariablesSingle(string data) {
|
||||||
|
string[string] va;
|
||||||
|
auto varArray = decodeVariables(data);
|
||||||
|
foreach(k, v; varArray)
|
||||||
|
va[k] = v[$-1];
|
||||||
|
|
||||||
|
return va;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// url encodes the whole string
|
||||||
|
string encodeVariables(in string[string] data) {
|
||||||
|
string ret;
|
||||||
|
|
||||||
|
bool outputted = false;
|
||||||
|
foreach(k, v; data) {
|
||||||
|
if(outputted)
|
||||||
|
ret ~= "&";
|
||||||
|
else
|
||||||
|
outputted = true;
|
||||||
|
|
||||||
|
ret ~= encodeUriComponent(k) ~ "=" ~ encodeUriComponent(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// url encodes a whole string
|
||||||
|
string encodeVariables(in string[][string] data) {
|
||||||
|
string ret;
|
||||||
|
|
||||||
|
bool outputted = false;
|
||||||
|
foreach(k, arr; data) {
|
||||||
|
foreach(v; arr) {
|
||||||
|
if(outputted)
|
||||||
|
ret ~= "&";
|
||||||
|
else
|
||||||
|
outputted = true;
|
||||||
|
ret ~= encodeUriComponent(k) ~ "=" ~ encodeUriComponent(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes all but the explicitly unreserved characters per rfc 3986
|
||||||
|
/// Alphanumeric and -_.~ are the only ones left unencoded
|
||||||
|
/// name is borrowed from php
|
||||||
|
string rawurlencode(in char[] data) {
|
||||||
|
string ret;
|
||||||
|
ret.reserve(data.length * 2);
|
||||||
|
foreach(char c; data) {
|
||||||
|
if(
|
||||||
|
(c >= 'a' && c <= 'z') ||
|
||||||
|
(c >= 'A' && c <= 'Z') ||
|
||||||
|
(c >= '0' && c <= '9') ||
|
||||||
|
c == '-' || c == '_' || c == '.' || c == '~')
|
||||||
|
{
|
||||||
|
ret ~= c;
|
||||||
|
} else {
|
||||||
|
ret ~= '%';
|
||||||
|
// since we iterate on char, this should give us the octets of the full utf8 string
|
||||||
|
ret ~= toHexUpper(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
char[2] toHexUpper(ubyte num) {
|
||||||
|
char[2] ret = 0;
|
||||||
|
ret[0] = num / 16;
|
||||||
|
ret[1] = num % 16;
|
||||||
|
ret[0] += cast(char)(ret[0] >= 10 ? 'A' : '0');
|
||||||
|
ret[1] += cast(char)(ret[1] >= 10 ? 'A' : '0');
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
2
web.d
2
web.d
|
|
@ -11,6 +11,8 @@
|
||||||
+/
|
+/
|
||||||
module arsd.web;
|
module arsd.web;
|
||||||
|
|
||||||
|
import arsd.uri : decodeVariablesSingle, encodeVariables;
|
||||||
|
|
||||||
static if(__VERSION__ <= 2076) {
|
static if(__VERSION__ <= 2076) {
|
||||||
// compatibility shims with gdc
|
// compatibility shims with gdc
|
||||||
enum JSONType {
|
enum JSONType {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ module arsd.webtemplate;
|
||||||
|
|
||||||
import arsd.script;
|
import arsd.script;
|
||||||
import arsd.dom;
|
import arsd.dom;
|
||||||
|
import arsd.uri;
|
||||||
|
|
||||||
public import arsd.jsvar : var;
|
public import arsd.jsvar : var;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue