Mon 21 Jul 22:43:21 CEST 2025

This commit is contained in:
sbosse 2025-07-21 23:19:46 +02:00
parent 2d1983bcfa
commit 03cb072763

813
js/ui/webui/winshell.js Normal file
View File

@ -0,0 +1,813 @@
// JAM Shell Wrapper Version 1.1.2
function winShell(opts) {
opts = opts || {};
var options = {
cmd : null,
name : "jam",
nameopts : {length:8, memorable:true, lowercase:true},
Nameopts : {length:8, memorable:true, uppercase:true},
log : { Time: true, Network: false },
output : printAnswer,
server : true,
}
// http://www.squarefree.com/shell/shell.html
var
histList = [""],
histPos = 0,
_scope = {},
_win, // a top-level context
question,
_in,
_out,
_scroll,
tooManyMatches = null,
lastError = null;
function refocus() {
if (!_in) return;
_in.blur(); // Needed for Mozilla to scroll correctly.
_in.focus();
}
function scrollToBottom(id){
var div = document.getElementById(id);
div.scrollTop = div.scrollHeight - div.clientHeight;
}
function setup () {
}
function init(input, output, scroll) {
if (input) _in = document.getElementById(input);
_out = document.getElementById(output);
_scroll = scroll;
_win = window;
if (opener && !opener.closed) {
console.log("Using bookmarklet version of shell: commands will run in opener's context.", "message");
_win = opener;
}
initTarget();
recalculateInputHeight();
refocus();
if (options.shell == undefined) {
console.log('winshell: Creating jamsh ..')
options.shell = SHELL(options);
options.cmd = options.shell.init().cmd();
options.name = options.cmd.name('node');
} else {
// It is an agent monitor - attach to shell
options.cmd = options.shell.cmd();
options.cmd.config({printAgent:printAnswer});
options.name = options.cmd.name('node');
}
}
function initTarget() {
_win.Shell = window;
_win.print = shellCommands.print;
}
// Unless the user is selected something, refocus the textbox.
// (requested by caillon, brendan, asa)
function keepFocusInTextbox(e) {
var g = e.srcElement ? e.srcElement : e.target; // IE vs. standard
while (!g.tagName)
g = g.parentNode;
var t = g.tagName.toUpperCase();
if (t == "A" || t == "INPUT")
return;
if (window.getSelection) {
// Mozilla
if (String(window.getSelection()))
return;
} else if (document.getSelection) {
// Opera? Netscape 4?
if (document.getSelection())
return;
} else {
// IE
if (document.selection.createRange().text)
return;
}
refocus();
}
function inputKeydown(e) {
// Use onkeydown because IE doesn't support onkeypress for arrow keys
//alert(e.keyCode + " ^ " + e.keycode);
if (e.shiftKey && e.keyCode == 13) { // shift-enter
// don't do anything; allow the shift-enter to insert a line break as normal
} else if (e.keyCode == 13) { // enter
// execute the input on enter
try {
go();
} catch (er) {
alert(er);
};
setTimeout(function() {
_in.value = "";
}, 0); // can't preventDefault on input, so clear it later
} else if (e.keyCode == 38) { // up
// go up in history if at top or ctrl-up
if (e.ctrlKey || caretInFirstLine(_in))
hist(true);
} else if (e.keyCode == 40) { // down
// go down in history if at end or ctrl-down
if (e.ctrlKey || caretInLastLine(_in))
hist(false);
} else if (e.keyCode == 9) { // tab
tabcomplete();
setTimeout(function() {
refocus();
}, 0); // refocus because tab was hit
} else {}
setTimeout(recalculateInputHeight, 0);
//return true;
};
function caretInFirstLine(textbox) {
// IE doesn't support selectionStart/selectionEnd
if (textbox.selectionStart == undefined)
return true;
var firstLineBreak = textbox.value.indexOf("\n");
return ((firstLineBreak == -1) || (textbox.selectionStart <= firstLineBreak));
}
function caretInLastLine(textbox) {
// IE doesn't support selectionStart/selectionEnd
if (textbox.selectionEnd == undefined)
return true;
var lastLineBreak = textbox.value.lastIndexOf("\n");
return (textbox.selectionEnd > lastLineBreak);
}
function recalculateInputHeight() {
if (!_in) return;
var rows = _in.value.split(/\n/).length +
1 // prevent scrollbar flickering in Mozilla
+
(window.opera ? 1 : 0); // leave room for scrollbar in Opera
if (_in.rows != rows) // without this check, it is impossible to select text in Opera 7.60 or Opera 8.0.
_in.rows = rows;
}
function loadScript(url, callback) {
var script = document.createElement("script")
script.type = "text/javascript";
if (script.readyState) { //IE
script.onreadystatechange = function() {
if (script.readyState == "loaded" ||
script.readyState == "complete") {
script.onreadystatechange = null;
callback();
}
};
} else { //Others
script.onload = function() {
callback();
};
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
}
function get(url, callback) {
// read text from URL location
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.send(null);
request.onreadystatechange = function() {
if (request.status==0) {
println('No connection to LUAOS.');
if (_listener) clearInterval(_listener);
} else if (request.readyState === 4) {
var type = request.getResponseHeader('Content-Type');
if (type && type.indexOf("text") !== 1) {
callback(request.responseText);
}
}
}
}
function put(url, data, callback) {
// write text to URL location
var request = new XMLHttpRequest();
request.open('POST', url, true);
request.send(data);
request.onreadystatechange = function(err) {
if (request.status==0) {
println('No connection to LUAOS.');
if (_listener) clearInterval(_listener);
} else if (request.readyState === 4 && request.status === 200) {
callback(request.responseText);
}
}
}
function println(s, type, time) {
if ((s = String(s))) {
var newdiv = document.createElement("div");
newdiv.innerHTML=
(time?'<span style="color:green">'+SHELL.Io.Time()+'</span> ':'')+
s.replace(/&/g,'&amp;')
.replace(/\t/g,' ')
.replace(/\s/g,'&nbsp;')
.replace(/</g,'&lt;')
.replace(/>/g,'&gt;');
newdiv.className = type;
_out.appendChild(newdiv);
if (_scroll && _scroll.scrollAuto) {
var scPos = _scroll.getScrollState();
_scroll.scrollTo(0,scPos.y+500);
}
return newdiv;
}
}
function printWithRunin(h, s, type) {
var div = println(s, type);
var head = document.createElement("strong");
head.appendChild(document.createTextNode(h + ": "));
div.insertBefore(head, div.firstChild);
}
function iter(a, func) {
for (var p in a) func(a[p])
}
var shellCommands = {
load: function load(url) {
var s = _win.document.createElement("script");
s.type = "text/javascript";
s.src = url;
_win.document.getElementsByTagName("head")[0].appendChild(s);
println("Loading " + url + "...", "message");
},
clear: function clear() {
var CHILDREN_TO_PRESERVE = 0;
while (_out.childNodes[CHILDREN_TO_PRESERVE])
_out.removeChild(_out.childNodes[CHILDREN_TO_PRESERVE]);
},
code: function code(index) {
if (location.href.split('?').length == 1) return;
var params = location.href.split('?')[1].split('&');
var data = {},
i = 1,
file;
for (var x in params) {
data[params[x].split('=')[0]] = params[x].split('=')[1];
if (i == index) file = data[params[x].split('=')[0]];
i++;
}
if (file) {
get('code/' + file, function(text) {
go(text)
});
}
},
codes: function codes() {
if (location.href.split('?').length == 1) return;
var params = location.href.split('?')[1].split('&');
var data = {};
for (var x in params) {
data[params[x].split('=')[0]] = params[x].split('=')[1];
}
return data;
},
getResult: function getResult() {
get(URL, function(reply) {
printAnswer(reply);
});
},
help: function help(topic) {
printWithRunin("Features", "autocompletion of property names with Tab,");
printWithRunin("Features", "multiline input with Shift+Enter, input history with (Ctrl+) Up/Down");
},
print: function print(s) {
println(s, "print");
},
reset: function reset() {
put(URL, ':reset');
},
// the normal function, "print", shouldn't return a value
// (suggested by brendan; later noticed it was a problem when showing others)
pr: function pr(s) {
shellCommands.print(s); // need to specify shellCommands so it doesn't try window.print()!
return s;
},
props: function props(e, onePerLine) {
if (e === null) {
println("props called with null argument", "error");
return;
}
if (e === undefined) {
println("props called with undefined argument", "error");
return;
}
var ns = ["Methods", "Fields", "Unreachables"];
var as = [
[],
[],
[]
]; // array of (empty) arrays of arrays!
var p, j, i; // loop variables, several used multiple times
var protoLevels = 0;
for (p = e; p; p = p.__proto__) {
for (i = 0; i < ns.length; ++i)
as[i][protoLevels] = [];
++protoLevels;
}
for (var a in e) {
// Shortcoming: doesn't check that VALUES are the same in object and prototype.
var protoLevel = -1;
try {
for (p = e; p && (a in p); p = p.__proto__)
++protoLevel;
} catch (er) {
protoLevel = 0;
} // "in" operator throws when param to props() is a string
var type = 1;
try {
if ((typeof e[a]) == "function")
type = 0;
} catch (er) {
type = 2;
}
as[type][protoLevel].push(a);
}
function times(s, n) {
return n ? s + times(s, n - 1) : "";
}
for (j = 0; j < protoLevels; ++j)
for (i = 0; i < ns.length; ++i)
if (as[i][j].length)
printWithRunin(
ns[i] + times(" of prototype", j),
(onePerLine ? "\n\n" : "") + as[i][j].sort().join(onePerLine ? "\n" : ", ") + (onePerLine ? "\n\n" : ""),
"propList"
);
},
blink: function blink(node) {
if (!node) throw ("blink: argument is null or undefined.");
if (node.nodeType == null) throw ("blink: argument must be a node.");
if (node.nodeType == 3) throw ("blink: argument must not be a text node");
if (node.documentElement) throw ("blink: argument must not be the document object");
function setOutline(o) {
return function() {
if (node.style.outline != node.style.bogusProperty) {
// browser supports outline (Firefox 1.1 and newer, CSS3, Opera 8).
node.style.outline = o;
} else if (node.style.MozOutline != node.style.bogusProperty) {
// browser supports MozOutline (Firefox 1.0.x and older)
node.style.MozOutline = o;
} else {
// browser only supports border (IE). border is a fallback because it moves things around.
node.style.border = o;
}
}
}
function focusIt(a) {
return function() {
a.focus();
}
}
if (node.ownerDocument) {
var windowToFocusNow = (node.ownerDocument.defaultView || node.ownerDocument.parentWindow); // Moz vs. IE
if (windowToFocusNow)
setTimeout(focusIt(windowToFocusNow.top), 0);
}
for (var i = 1; i < 7; ++i)
setTimeout(setOutline((i % 2) ? '3px solid red' : 'none'), i * 100);
setTimeout(focusIt(window), 800);
if (_in) setTimeout(focusIt(_in), 810);
},
ans: undefined
};
function hist(up) {
// histList[0] = first command entered, [1] = second, etc.
// type something, press up --> thing typed is now in "limbo"
// (last item in histList) and should be reachable by pressing
// down again.
var L = histList.length;
if (L == 1)
return;
if (up) {
if (histPos == L - 1) {
// Save this entry in case the user hits the down key.
histList[histPos] = _in.value;
}
if (histPos > 0) {
histPos--;
// Use a timeout to prevent up from moving cursor within new text
// Set to nothing first for the same reason
setTimeout(
function() {
_in.value = '';
_in.value = histList[histPos];
var caretPos = _in.value.length;
if (_in.setSelectionRange)
_in.setSelectionRange(caretPos, caretPos);
},
0
);
}
} else // down
{
if (histPos < L - 1) {
histPos++;
_in.value = histList[histPos];
} else if (histPos == L - 1) {
// Already on the current entry: clear but save
if (_in.value) {
histList[histPos] = _in.value;
++histPos;
_in.value = "";
}
}
}
}
function tabcomplete() {
/*
* Working backwards from s[from], find the spot
* where this expression starts. It will scan
* until it hits a mismatched ( or a space,
* but it skips over quoted strings.
* If stopAtDot is true, stop at a '.'
*/
function findbeginning(s, from, stopAtDot) {
/*
* Complicated function.
*
* Return true if s[i] == q BUT ONLY IF
* s[i-1] is not a backslash.
*/
function equalButNotEscaped(s, i, q) {
if (s.charAt(i) != q) // not equal go no further
return false;
if (i == 0) // beginning of string
return true;
if (s.charAt(i - 1) == '\\') // escaped?
return false;
return true;
}
var nparens = 0;
var i;
for (i = from; i >= 0; i--) {
if (s.charAt(i) == ' ')
break;
if (stopAtDot && s.charAt(i) == '.')
break;
if (s.charAt(i) == ')')
nparens++;
else if (s.charAt(i) == '(')
nparens--;
if (nparens < 0)
break;
// skip quoted strings
if (s.charAt(i) == '\'' || s.charAt(i) == '\"') {
//dump("skipping quoted chars: ");
var quot = s.charAt(i);
i--;
while (i >= 0 && !equalButNotEscaped(s, i, quot)) {
//dump(s.charAt(i));
i--;
}
//dump("\n");
}
}
return i;
}
// XXX should be used more consistently (instead of using selectionStart/selectionEnd throughout code)
// XXX doesn't work in IE, even though it contains IE-specific code
function getcaretpos(inp) {
if (inp.selectionEnd != null)
return inp.selectionEnd;
if (inp.createTextRange) {
var docrange = _win.Shell.document.selection.createRange();
var inprange = inp.createTextRange();
if (inprange.setEndPoint) {
inprange.setEndPoint('EndToStart', docrange);
return inprange.text.length;
}
}
return inp.value.length; // sucks, punt
}
function setselectionto(inp, pos) {
if (inp.selectionStart) {
inp.selectionStart = inp.selectionEnd = pos;
} else if (inp.createTextRange) {
var docrange = _win.Shell.document.selection.createRange();
var inprange = inp.createTextRange();
inprange.move('character', pos);
inprange.select();
} else { // err...
/*
inp.select();
if(_win.Shell.document.getSelection())
_win.Shell.document.getSelection() = "";
*/
}
}
// get position of cursor within the input box
var caret = getcaretpos(_in);
if (caret) {
//dump("----\n");
var dotpos, spacepos, complete, obj;
//dump("caret pos: " + caret + "\n");
// see if there's a dot before here
dotpos = findbeginning(_in.value, caret - 1, true);
//dump("dot pos: " + dotpos + "\n");
if (dotpos == -1 || _in.value.charAt(dotpos) != '.') {
dotpos = caret;
//dump("changed dot pos: " + dotpos + "\n");
}
// look backwards for a non-variable-name character
spacepos = findbeginning(_in.value, dotpos - 1, false);
//dump("space pos: " + spacepos + "\n");
// get the object we're trying to complete on
if (spacepos == dotpos || spacepos + 1 == dotpos || dotpos == caret) {
// try completing function args
if (_in.value.charAt(dotpos) == '(' ||
(_in.value.charAt(spacepos) == '(' && (spacepos + 1) == dotpos)) {
var fn, fname;
var from = (_in.value.charAt(dotpos) == '(') ? dotpos : spacepos;
spacepos = findbeginning(_in.value, from - 1, false);
fname = _in.value.substr(spacepos + 1, from - (spacepos + 1));
//dump("fname: " + fname + "\n");
try {
with(_win.Shell._scope)
with(_win)
with(shellCommands)
fn = eval(fname);
} catch (er) {
//dump('fn is not a valid object\n');
return;
}
if (fn == undefined) {
//dump('fn is undefined');
return;
}
if (fn instanceof Function) {
// Print function definition, including argument names, but not function body
if (!fn.toString().match(/function .+?\(\) +\{\n +\[native code\]\n\}/))
println(fn.toString().match(/function .+?\(.*?\)/), "tabcomplete");
}
return;
} else
obj = _win;
} else {
var objname = _in.value.substr(spacepos + 1, dotpos - (spacepos + 1));
//dump("objname: |" + objname + "|\n");
try {
with(_win.Shell._scope)
with(_win)
obj = eval(objname);
} catch (er) {
printError(er);
return;
}
if (obj == undefined) {
// sometimes this is tabcomplete's fault, so don't print it :(
// e.g. completing from "print(document.getElements"
// println("Can't complete from null or undefined expression " + objname, "error");
return;
}
}
//dump("obj: " + obj + "\n");
// get the thing we're trying to complete
if (dotpos == caret) {
if (spacepos + 1 == dotpos || spacepos == dotpos) {
// nothing to complete
//dump("nothing to complete\n");
return;
}
complete = _in.value.substr(spacepos + 1, dotpos - (spacepos + 1));
} else {
complete = _in.value.substr(dotpos + 1, caret - (dotpos + 1));
}
//dump("complete: " + complete + "\n");
// ok, now look at all the props/methods of this obj
// and find ones starting with 'complete'
var matches = [];
var bestmatch = null;
for (var a in obj) {
//a = a.toString();
//XXX: making it lowercase could help some cases,
// but screws up my general logic.
if (a.substr(0, complete.length) == complete) {
matches.push(a);
////dump("match: " + a + "\n");
// if no best match, this is the best match
if (bestmatch == null) {
bestmatch = a;
} else {
// the best match is the longest common string
function min(a, b) {
return ((a < b) ? a : b);
}
var i;
for (i = 0; i < min(bestmatch.length, a.length); i++) {
if (bestmatch.charAt(i) != a.charAt(i))
break;
}
bestmatch = bestmatch.substr(0, i);
////dump("bestmatch len: " + i + "\n");
}
////dump("bestmatch: " + bestmatch + "\n");
}
}
bestmatch = (bestmatch || "");
////dump("matches: " + matches + "\n");
var objAndComplete = (objname || obj) + "." + bestmatch;
//dump("matches.length: " + matches.length + ", tooManyMatches: " + tooManyMatches + ", objAndComplete: " + objAndComplete + "\n");
if (matches.length > 1 && (tooManyMatches == objAndComplete || matches.length <= 10)) {
printWithRunin("Matches: ", matches.join(', '), "tabcomplete");
tooManyMatches = null;
} else if (matches.length > 10) {
println(matches.length + " matches. Press tab again to see them all", "tabcomplete");
tooManyMatches = objAndComplete;
} else {
tooManyMatches = null;
}
if (bestmatch != "") {
var sstart;
if (dotpos == caret) {
sstart = spacepos + 1;
} else {
sstart = dotpos + 1;
}
_in.value = _in.value.substr(0, sstart) +
bestmatch +
_in.value.substr(caret);
setselectionto(_in, caret + (bestmatch.length - complete.length));
}
}
}
function printQuestion(q) {
println(q, "input");
}
function printAnswer(a) {
if (a !== undefined) {
a.split('\n').forEach(function (line,index) {
if (opts.cmd && line[0]=='@') {
opts.cmd(line);
} else {
println(line, "normalOutput", options.log.Time && index==0)
}
});
shellCommands.ans = a;
}
}
function printError(er) {
var lineNumberString;
lastError = er; // for debugging the shell
if (er.name) {
// lineNumberString should not be "", to avoid a very wacky bug in IE 6.
lineNumberString = (er.lineNumber != undefined) ? (" on line " + er.lineNumber + ": ") : ": ";
println(er.name + lineNumberString + er.message, "error"); // Because IE doesn't have error.toString.
} else
println(er, "error"); // Because security errors in Moz /only/ have toString.
}
function go(s) {
_in.value = question = s ? s : _in.value;
if (question == "")
return;
histList[histList.length - 1] = question;
histList[histList.length] = "";
histPos = histList.length - 1;
// Unfortunately, this has to happen *before* the JavaScript is run, so that
// print() output will go in the right place.
_in.value = '';
recalculateInputHeight();
printQuestion(question);
if (_win.closed) {
printError("Target window has been closed.");
return;
}
try {
("Shell" in _win)
} catch (er) {
printError("The JavaScript Shell cannot access variables in the target window. The most likely reason is that the target window now has a different page loaded and that page has a different hostname than the original page.");
return;
}
if (!("Shell" in _win))
initTarget(); // silent
// Evaluate question using _win's eval (this is why eval isn't in the |with|, IIRC).
// _win.location.href = "javascript:try{ Shell.printAnswer(eval('with(Shell._scope) with(Shell.shellCommands) {' + Shell.question + String.fromCharCode(10) + '}')); } catch(er) { Shell.printError(er); }; setTimeout(Shell.refocus, 0); void 0";
options.cmd.exec(question)
}
function ask(cmd,data,callback) {
options.cmd.exec(cmd)
}
function cmd () {
return options.cmd
}
// API
return {
ask:ask,
cmd:cmd,
commands: shellCommands,
init: init,
inputKeydown: inputKeydown,
options:options,
print:printAnswer,
setup:setup
}
};