During my honors, I took a couple of classes involving Human Computer Interaction (HCI). HCI in a sense is the study of how somebody acts around technology and what results come of it. What had been troubling me in the tooling area up to this point, was the fact that to add new source locations to dub repo involved editing source code. It didn't seem right to me, so I went about trying to fix this (in UI design).
Show/Hide example code
module schemes;
import std.array : appender;
///
struct SchemeVars {
///
string workingDirectory;
///
string serverURL;
}
///
struct Scheme {
///
string name, varient;
private {
SchemePerform getFile_, getArchive_, getTags_, getBranches_;
SchemeView getTagsView_, getBranchesView_;
string defaultServerURL;
}
///
this(string name, string varient, string defaultServerURL,
SchemePerform getFile_, SchemePerform getArchive_,
SchemePerform getTags_, SchemeView getTagsView_,
SchemePerform getBranches_, SchemeView getBranchesView_) {
this.name = name;
this.varient = varient;
this.defaultServerURL = defaultServerURL;
this.getFile_ = getFile_;
this.getArchive_ = getArchive_;
this.getTags_ = getTags_;
this.getTagsView_ = getTagsView_;
this.getBranches_ = getBranches_;
this.getBranchesView_ = getBranchesView_;
}
///
this(ref Scheme other, string varient, string defaultServerURL) {
this.name = other.name;
this.varient = varient;
this.defaultServerURL = defaultServerURL;
this.getFile_ = other.getFile_;
this.getArchive_ = other.getArchive_;
this.getTags_ = other.getTags_;
this.getTagsView_ = other.getTagsView_;
this.getBranches_ = other.getBranches_;
this.getBranchesView_ = other.getBranchesView_;
}
///
this(ref Scheme other, string varient, string defaultServerURL, SchemePerform getFile_) {
this(other, varient, defaultServerURL);
this.getFile_ = getFile_;
}
///
this(ref Scheme other, string varient, string defaultServerURL,
SchemePerform getFile_, SchemePerform getArchive_) {
this(other, varient, defaultServerURL);
this.getFile_ = getFile_;
this.getArchive_ = other.getArchive_;
}
///
ubyte[] getFile(string username, string repository, string id, string filename, SchemeVars vars=SchemeVars()) {
string serverURL = vars.serverURL !is null ? vars.serverURL : defaultServerURL;
if (getFile_.isURL) {
import std.net.curl : get, AutoProtocol;
auto url = appender!string();
url.reserve(128);
string[] parts;
if (parts is null) {
foreach(tag; getTags(username, repository)) {
if (tag.id == id) {
parts = getFile_.partsForTag;
}
}
}
if (parts is null) {
foreach(branch; getBranches(username, repository)) {
if (branch.id == id) {
parts = getFile_.partsForBranch;
}
}
}
if (parts is null) {
parts = getFile_.partsForElse;
}
foreach(i, p; parts) {
if (p.length >= 10 && p[0 .. 10] == "%USERNAME%") {
url ~= username;
if (p.length == 11)
url ~= '/';
} else if (p.length >= 12 && p[0 .. 12] == "%REPOSITORY%") {
url ~= repository;
if (p.length == 13)
url ~= '/';
} else if (p.length >= 4 && p[0 .. 4] == "%ID%") {
url ~= id;
if (p.length == 5)
url ~= '/';
} else if (p.length >= 10 && p[0 .. 10] == "%FILENAME%") {
url ~= filename;
if (p.length == 11)
url ~= '/';
} else if (p.length >= 5 && p[0 .. 5] == "%URL%") {
url ~= serverURL;
if (p.length == 6)
url ~= '/';
} else {
url ~= p;
}
}
try {
return get!(AutoProtocol, ubyte)(url.data);
} catch (Exception e) {
return null;
}
} else if (getFile_.isComplex) {
import std.file : write;
ubyte[] theoutput;
foreach(step; getFile_.steps) {
StartOfStep:
if (step.isWriteFile) {
auto contents = appender!string();
contents.reserve(1024 * 1024 * 4); // 4mb
foreach(i, p; step.writeParts) {
if (p.length == 10 && p[0 .. 10] == "%USERNAME%") {
contents ~= username;
} else if (p.length == 12 && p[0 .. 12] == "%REPOSITORY%") {
contents ~= repository;
} else if (p.length == 4 && p[0 .. 4] == "%ID%") {
contents ~= id;
} else if (p.length == 10 && p[0 .. 10] == "%FILENAME%") {
contents ~= filename;
} else if (p.length >= 12 && p[0 .. 12] == "%WORKINGDIR%") {
contents ~= vars.workingDirectory;
} else if (p.length >= 5 && p[0 .. 5] == "%URL%") {
contents ~= serverURL;
} else {
contents ~= p;
}
}
write(step.writeName, contents.data);
} else if (step.isReadFile) {
import std.file : read;
if (step.appendNotAssignOutput) {
theoutput ~= cast(ubyte[])read(step.readName);
} else {
theoutput = cast(ubyte[])read(step.readName);
}
} else if (step.isExecute) {
import std.process : execute;
string[] args;
args ~= step.program;
foreach(i, p; step.programArgs) {
if (p[0] == '~')
p = p[1 .. $];
else
args.length++;
if (p.length >= 10 && p[0 .. 10] == "%USERNAME%") {
args[$-1] ~= username;
if (p.length == 11)
args[$-1] ~= '/';
} else if (p.length >= 12 && p[0 .. 12] == "%REPOSITORY%") {
args[$-1] ~= repository;
if (p.length == 13)
args[$-1] ~= '/';
} else if (p.length >= 4 && p[0 .. 4] == "%ID%") {
args[$-1] ~= id;
if (p.length == 5)
args[$-1] ~= '/';
} else if (p.length >= 10 && p[0 .. 10] == "%FILENAME%") {
args[$-1] ~= filename;
if (p.length == 11)
args[$-1] ~= '/';
} else if (p.length >= 12 && p[0 .. 12] == "%WORKINGDIR%") {
args[$-1] ~= vars.workingDirectory;
if (p.length == 13)
args[$-1] ~= '/';
} else if (p.length >= 5 && p[0 .. 5] == "%URL%") {
args[$-1] ~= serverURL;
if (p.length == 6)
args[$-1] ~= '/';
} else {
args[$-1] ~= p;
}
}
auto called = execute(args);
if (step.appendNotAssignOutput) {
theoutput ~= cast(ubyte[])called.output;
} else {
theoutput = cast(ubyte[])called.output;
}
if (step.programOutputToFile !is null) {
write(step.programOutputToFile, called.output);
}
} else if (step.isConstant) {
if (step.appendNotAssignOutput) {
theoutput ~= cast(ubyte[])step.constant;
} else {
theoutput = cast(ubyte[])step.constant;
}
} else if (step.isConditional) {
foreach(tag; getTags(username, repository)) {
if (tag.id == id) {
step = *step.isTag;
goto StartOfStep;
}
}
foreach(branch; getBranches(username, repository)) {
if (branch.id == id) {
step = *step.isBranch;
goto StartOfStep;
}
}
step = *step.isElse;
goto StartOfStep;
}
}
return theoutput;
}
return null;
}
///
string getArchiveURL(string username, string repository, string id, SchemeVars vars=SchemeVars()) {
string serverURL = vars.serverURL !is null ? vars.serverURL : defaultServerURL;
if (getArchive_.isURL) {
import std.net.curl : get, AutoProtocol;
auto url = appender!string();
url.reserve(128);
string[] parts;
if (parts is null) {
foreach(tag; getTags(username, repository)) {
if (tag.id == id) {
parts = getFile_.partsForTag;
}
}
}
if (parts is null) {
foreach(branch; getBranches(username, repository)) {
if (branch.id == id) {
parts = getFile_.partsForBranch;
}
}
}
if (parts is null) {
parts = getFile_.partsForElse;
}
foreach(i, p; parts) {
if (p.length >= 10 && p[0 .. 10] == "%USERNAME%") {
url ~= username;
if (p.length == 11)
url ~= '/';
} else if (p.length >= 12 && p[0 .. 12] == "%REPOSITORY%") {
url ~= repository;
if (p.length == 13)
url ~= '/';
} else if (p.length >= 4 && p[0 .. 4] == "%ID%") {
url ~= id;
if (p.length == 5)
url ~= '/';
} else if (p.length >= 5 && p[0 .. 5] == "%URL%") {
url ~= serverURL;
if (p.length == 6)
url ~= '/';
} else {
url ~= p;
}
}
return url.data;
}
return null;
}
///
ubyte[] getArchive(string username, string repository, string id, SchemeVars vars=SchemeVars()) {
string serverURL = vars.serverURL !is null ? vars.serverURL : defaultServerURL;
if (getArchive_.isURL) {
import std.net.curl : get, AutoProtocol;
auto url = appender!string();
url.reserve(128);
string[] parts;
if (parts is null) {
foreach(tag; getTags(username, repository)) {
if (tag.id == id) {
parts = getArchive_.partsForTag;
}
}
}
if (parts is null) {
foreach(branch; getBranches(username, repository)) {
if (branch.id == id) {
parts = getArchive_.partsForBranch;
}
}
}
if (parts is null) {
parts = getArchive_.partsForElse;
}
foreach(i, p; parts) {
if (p.length >= 10 && p[0 .. 10] == "%USERNAME%") {
url ~= username;
if (p.length == 11)
url ~= '/';
} else if (p.length >= 12 && p[0 .. 12] == "%REPOSITORY%") {
url ~= repository;
if (p.length == 13)
url ~= '/';
} else if (p.length >= 4 && p[0 .. 4] == "%ID%") {
url ~= id;
if (p.length == 5)
url ~= '/';
} else if (p.length >= 5 && p[0 .. 5] == "%URL%") {
url ~= serverURL;
if (p.length == 6)
url ~= '/';
} else {
url ~= p;
}
}
try {
return get!(AutoProtocol, ubyte)(url.data);
} catch (Exception e) {
return null;
}
} else if (getArchive_.isComplex) {
import std.file : write;
ubyte[] theoutput;
foreach(step; getArchive_.steps) {
StartOfStep:
if (step.isWriteFile) {
auto contents = appender!string();
contents.reserve(1024 * 1024 * 4); // 4mb
foreach(i, p; step.writeParts) {
if (p.length == 10 && p[0 .. 10] == "%USERNAME%") {
contents ~= username;
} else if (p.length == 12 && p[0 .. 12] == "%REPOSITORY%") {
contents ~= repository;
} else if (p.length == 4 && p[0 .. 4] == "%ID%") {
contents ~= id;
} else if (p.length == 12 && p[0 .. 12] == "%WORKINGDIR%") {
contents ~= vars.workingDirectory;
} else if (p.length >= 5 && p[0 .. 5] == "%URL%") {
contents ~= serverURL;
} else {
contents ~= p;
}
}
write(step.writeName, contents.data);
} else if (step.isReadFile) {
import std.file : read;
if (step.appendNotAssignOutput) {
theoutput ~= cast(ubyte[])read(step.readName);
} else {
theoutput = cast(ubyte[])read(step.readName);
}
} else if (step.isExecute) {
import std.process : execute;
string[] args;
args ~= step.program;
foreach(i, p; step.programArgs) {
if (p[0] == '~')
p = p[1 .. $];
else
args.length++;
if (p.length >= 10 && p[0 .. 10] == "%USERNAME%") {
args[$-1] ~= username;
if (p.length == 11)
args[$-1] ~= '/';
} else if (p.length >= 12 && p[0 .. 12] == "%REPOSITORY%") {
args[$-1] ~= repository;
if (p.length == 13)
args[$-1] ~= '/';
} else if (p.length >= 4 && p[0 .. 4] == "%ID%") {
args[$-1] ~= id;
if (p.length == 5)
args[$-1] ~= '/';
} else if (p.length >= 12 && p[0 .. 12] == "%WORKINGDIR%") {
args[$-1] ~= vars.workingDirectory;
if (p.length == 13)
args[$-1] ~= '/';
} else if (p.length >= 5 && p[0 .. 5] == "%URL%") {
args[$-1] ~= serverURL;
if (p.length == 6)
args[$-1] ~= '/';
} else {
args[$-1] ~= p;
}
}
import std.stdio;
writeln(args);
auto called = execute(args);
if (step.appendNotAssignOutput) {
theoutput ~= cast(ubyte[])called.output;
} else {
theoutput = cast(ubyte[])called.output;
}
if (step.programOutputToFile !is null) {
write(step.programOutputToFile, called.output);
}
} else if (step.isConstant) {
if (step.appendNotAssignOutput) {
theoutput ~= cast(ubyte[])step.constant;
} else {
theoutput = cast(ubyte[])step.constant;
}
} else if (step.isConditional) {
foreach(tag; getTags(username, repository)) {
if (tag.id == id) {
step = *step.isTag;
goto StartOfStep;
}
}
foreach(branch; getBranches(username, repository)) {
if (branch.id == id) {
step = *step.isBranch;
goto StartOfStep;
}
}
step = *step.isElse;
goto StartOfStep;
}
}
return theoutput;
}
return null;
}
///
SchemeId[] getTags(string username, string repository, SchemeVars vars=SchemeVars()) {
return getIds(getTags_, getTagsView_, username, repository, vars);
}
///
SchemeId[] getBranches(string username, string repository, SchemeVars vars=SchemeVars()) {
return getIds(getBranches_, getBranchesView_, username, repository, vars);
}
private SchemeId[] getIds(SchemePerform root, SchemeView rootView, string username, string repository, SchemeVars vars=SchemeVars()) {
char[] allidsText;
string serverURL = vars.serverURL !is null ? vars.serverURL : defaultServerURL;
if (root.isURL) {
import std.net.curl : get, AutoProtocol;
auto url = appender!string();
url.reserve(128);
foreach(i, p; root.partsForElse) {
if (p.length >= 10 && p[0 .. 10] == "%USERNAME%") {
url ~= username;
if (p.length == 11)
url ~= '/';
} else if (p.length >= 12 && p[0 .. 12] == "%REPOSITORY%") {
url ~= repository;
if (p.length == 13)
url ~= '/';
} else if (p.length >= 5 && p[0 .. 5] == "%URL%") {
url ~= serverURL;
if (p.length == 6)
url ~= '/';
} else {
url ~= p;
}
}
try {
allidsText = get(url.data);
} catch (Exception e) {
return null;
}
} else if (root.isComplex) {
import std.file : write;
ubyte[] theoutput;
foreach(step; root.steps) {
if (step.isWriteFile) {
auto contents = appender!string();
contents.reserve(1024 * 1024 * 4); // 4mb
foreach(i, p; step.writeParts) {
if (p.length == 10 && p[0 .. 10] == "%USERNAME%") {
contents ~= username;
} else if (p.length == 12 && p[0 .. 12] == "%REPOSITORY%") {
contents ~= repository;
} else if (p.length >= 5 && p[0 .. 5] == "%URL%") {
contents ~= serverURL;
} else {
contents ~= p;
}
}
write(step.writeName, contents.data);
} else if (step.isReadFile) {
import std.file : read;
if (step.appendNotAssignOutput) {
theoutput ~= cast(ubyte[])read(step.readName);
} else {
theoutput = cast(ubyte[])read(step.readName);
}
} else if (step.isExecute) {
import std.process : execute;
string[] args;
args ~= step.program;
foreach(i, p; step.programArgs) {
if (p[0] == '~')
p = p[1 .. $];
else
args.length++;
if (p.length >= 10 && p[0 .. 10] == "%USERNAME%") {
args[$-1] ~= username;
if (p.length == 11)
args[$-1] ~= '/';
} else if (p.length >= 12 && p[0 .. 12] == "%REPOSITORY%") {
args[$-1] ~= repository;
if (p.length == 13)
args[$-1] ~= '/';
} else if (p.length >= 5 && p[0 .. 5] == "%URL%") {
args[$-1] ~= serverURL;
if (p.length == 6)
args[$-1] ~= '/';
} else {
args[$-1] ~= p;
}
}
auto called = execute(args);
if (step.appendNotAssignOutput) {
theoutput ~= cast(ubyte[])called.output;
} else {
theoutput = cast(ubyte[])called.output;
}
if (step.programOutputToFile !is null) {
write(step.programOutputToFile, called.output);
}
} else if (step.isConstant) {
if (step.appendNotAssignOutput) {
theoutput ~= cast(ubyte[])step.constant;
} else {
theoutput = cast(ubyte[])step.constant;
}
}
}
allidsText = cast(char[])theoutput;
}
if (allidsText !is null) {
import std.string : lineSplitter;
SchemeId[] ret;
if (rootView.isDelimated) {
foreach(line; allidsText.lineSplitter) {
import std.algorithm : splitter;
if (rootView.requirePrefix) {
if (rootView.prefix == line[0 .. rootView.prefix.length])
line = line[rootView.prefix.length .. $];
else
continue;
}
string hash, id;
uint ofx;
foreach(v; line.splitter(rootView.deliminator)) {
if (ofx <= rootView.itemColumn.length) {
if (rootView.itemColumn[0] == ofx) {
id = cast(string)v;
} else if (rootView.itemColumn[1] == ofx) {
hash = cast(string)v;
}
}
ofx++;
}
if (hash !is null && id !is null) {
ret ~= SchemeId(id, hash);
}
}
} else {
foreach(line; allidsText.lineSplitter) {
ret ~= SchemeId(cast(string)line);
}
}
return ret;
}
return null;
}
}
///
struct SchemePerform {
///
bool isURL;
///
bool isComplex;
union {
struct {
///
string[] partsForTag, partsForBranch, partsForElse;
}
struct {
///
SchemePerformComplex[] steps;
}
}
///
static SchemePerform forURL(string[] parts) {
SchemePerform ret;
ret.isURL = true;
ret.partsForTag = parts;
ret.partsForBranch = parts;
ret.partsForElse = parts;
return ret;
}
///
static SchemePerform forURL(string[] partsForTag, string[] partsForBranch, string[] partsForElse) {
SchemePerform ret;
ret.isURL = true;
ret.partsForTag = partsForTag;
ret.partsForBranch = partsForBranch;
ret.partsForElse = partsForElse;
return ret;
}
///
static SchemePerform forComplex(SchemePerformComplex[] steps) {
SchemePerform ret;
ret.isComplex = true;
ret.steps = steps;
return ret;
}
}
///
struct SchemePerformComplex {
///
bool isWriteFile, isReadFile, isExecute, isConstant, isConditional;
///
bool appendNotAssignOutput;
union {
struct {
///
string writeName;
///
string[] writeParts;
}
struct {
///
string readName;
}
struct {
///
string program;
///
string[] programArgs;
///
string programOutputToFile;
}
struct {
string constant;
}
struct {
SchemePerformComplex* isTag, isBranch, isElse;
}
}
static {
///
SchemePerformComplex forWriteFile(string writeName, string[] writeParts) {
SchemePerformComplex ret;
ret.isWriteFile = true;
ret.writeName = writeName;
ret.writeParts = writeParts;
return ret;
}
///
SchemePerformComplex forReadFile(string readName, bool appendNotAssignOutput=false) {
SchemePerformComplex ret;
ret.isReadFile = true;
ret.readName = readName;
ret.appendNotAssignOutput = appendNotAssignOutput;
return ret;
}
///
SchemePerformComplex forExecute(string program, string[] programArgs, string programOutputToFile=null, bool appendNotAssignOutput=false) {
SchemePerformComplex ret;
ret.isExecute = true;
ret.program = program;
ret.programArgs = programArgs;
ret.programOutputToFile = programOutputToFile;
ret.appendNotAssignOutput = appendNotAssignOutput;
return ret;
}
///
SchemePerformComplex forConstant(string constant, bool appendNotAssignOutput=false) {
SchemePerformComplex ret;
ret.isConstant = true;
ret.constant = constant;
ret.appendNotAssignOutput = appendNotAssignOutput;
return ret;
}
///
SchemePerformComplex forConditional(SchemePerformComplex isTag, SchemePerformComplex isBranch, SchemePerformComplex isElse) {
SchemePerformComplex ret;
ret.isConditional = true;
ret.isTag = new SchemePerformComplex;
ret.isBranch = new SchemePerformComplex;
ret.isElse = new SchemePerformComplex;
*ret.isTag = isTag;
*ret.isBranch = isBranch;
*ret.isElse = isElse;
return ret;
}
}
}
///
struct SchemeView {
///
bool isDelimated;
///
bool requirePrefix;
///
char deliminator;
///
uint[2] itemColumn;
///
string prefix;
}
///
struct SchemeId {
///
string id;
///
string hash;
}
What I wanted was for a user to have a wonderful experience where the limiting factor of skills and knowledge is merely how to the destination storage system works. They could be a manager with no experience in systems management or programming and be able to throw together access to some historic or speciailized storage system for their team to use that day. Simplicity was the corner stone of the designs I tried to implement and its sole purpose is to try and automate the "scripting" framework happening behind the scenes. The basics of which are shown in the above code example.
So I made an example setup and prototyped upon that. Be warned, it sometimes takes a few times to load. It was a very volatile setup. The dynamic and volatile nature comes from my client-templating design. It was fully able to load up each different page of a site without URL change. But of course, the large number of requests produced a very unstable loading. It also would not have worked without Javascript enabled. All logging as disabled since completion of the survey. However you're free to try it out for prosperity sakes.
As part of the course (COMP626) we didn't just create a prototype. A design had to be created with HCI priniciples taken into account, reviwed and evolved. The process we used was the Five Sheet Design. Afterwards we would do a study involving the prototype, some people. I chose in doing an online survey given the prototype. A fairly simple method to do an HCI study. During analysis I determinined that while the prototype did its job (barely). The biggest problem with it is contextual help information. Quite simply, it did not manage to inform the user on how to use the interface even if fully capable (and step by step instructed by the survey). Finally I determined that I had not actually used any existing methodology but mashed together an in the wild approach and a formal laboratory experiment. I called this a "Wild Lab". An experiment where feedback is given constantly, instructions are given but no requirements are put onto the participant into doing so much. Everything or anything can be recorded but they are not restricted. This in some ways is similar to alpha/beta closed launches for web services.
What I learned from this, even the most seemingly easy interfaces and problems can quickly become hard in UI design for multistep systems and will rethink twice before attempting this again using web technologies.