// browser/anyterm.js // This file is part of Anyterm; see http://anyterm.org/ // (C) 2005-2006 Philip Endecott // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. var undefined; var url_prefix = ""; var frame; var term; var open=false; var session; var method="POST"; //var method="GET"; // Random sequence numbers are needed to prevent Opera from caching // replies var is_opera = navigator.userAgent.toLowerCase().indexOf("opera") != -1; if (is_opera) { method="GET"; } var seqnum_val=Math.round(Math.random()*100000); function cachebust() { if (is_opera) { seqnum_val++; return "&x="+seqnum_val; } else { return ""; } } // Cross-platform creation of XMLHttpRequest object: function new_XMLHttpRequest() { if (window.XMLHttpRequest) { // For most browsers: return new XMLHttpRequest(); } else { // For IE, it's active-X voodoo. // There are different versions in different browsers. // The ones we try are the ones that Sarissa tried. The disabled ones // apparently also exist, but it seems to work OK without trying them. //try{ return new ActiveXObject("MSXML3.XMLHTTP"); } catch(e){} try{ return new ActiveXObject("Msxml2.XMLHTTP.5.0"); } catch(e){} try{ return new ActiveXObject("Msxml2.XMLHTTP.4.0"); } catch(e){} try{ return new ActiveXObject("MSXML2.XMLHTTP.3.0"); } catch(e){} try{ return new ActiveXObject("MSXML2.XMLHTTP"); } catch(e){} //try{ return new ActiveXObject("Msxml2.XMLHTTP"); } catch(e){} try{ return new ActiveXObject("Microsoft.XMLHTTP"); } catch(e){} throw new Error("Could not find an XMLHttpRequest active-X class.") } } // Asynchronous and Synchronous XmlHttpRequest wrappers // AsyncLoader is a class; an instance specifies a callback function. // Call load to get something and the callback is invoked with the // returned document. function AsyncLoader(cb) { this.callback = cb; this.load = function (url,query) { var xmlhttp = new_XMLHttpRequest(); var cbk = this.callback; //var timeoutID = window.setTimeout("alert('No response after 20 secs')",20000); xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState==4) { //window.clearTimeout(timeoutID); if (xmlhttp.status==200) { cbk(xmlhttp.responseText); } else { alert("Server returned status code "+xmlhttp.status+":\n"+xmlhttp.statusText); cbk(null); } } } if (method=="GET") { xmlhttp.open(method, url+"?"+query, true); xmlhttp.send(null); } else if (method=="POST") { xmlhttp.open(method, url, true); xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xmlhttp.send(query); } } } // Synchronous loader is a simple function function sync_load(url,query) { var xmlhttp = new_XMLHttpRequest(); if (method=="GET") { xmlhttp.open(method, url+"?"+query, false); xmlhttp.send(null); } else if (method=="POST") { xmlhttp.open(method, url, false); xmlhttp.setRequestHeader('Foo','1234'); xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xmlhttp.send(query); } if (xmlhttp.status!=200) { alert("Server returned status code "+xmlhttp.status+":\n"+xmlhttp.statusText); return null; } return xmlhttp.responseText; } // Process error message from server: function handle_resp_error(resp) { if (resp.charAt(0)=="E") { var msg = resp.substr(1); alert(msg); return true; } return false; } // Receive channel: var rcv_loader; var disp=""; function process_editscript(edscr) { var ndisp=""; var i=0; var dp=0; while (i<edscr.length) { var cmd=edscr.charAt(i); i++; var cp=edscr.indexOf(":",i); var num=Number(edscr.substr(i,cp-i)); i=cp+1; //alert("cmd="+cmd+" num="+num); if (cmd=="d") { dp+=num; } else if (cmd=="k") { ndisp+=disp.substr(dp,num); dp+=num; } else if (cmd=="i") { //if (edscr.length<i+num) { //alert("edit script ended early; expecting "+num+" but got only "+edscr.length-cp); //} ndisp+=edscr.substr(i,num); i+=num; } } return ndisp; } var visible_height_frac = 1; function display(edscr) { //alert(edscr); var ndisp; if (edscr=="n") { return; } else if (edscr.charAt(0)=="R") { ndisp = edscr.substr(1); } else { ndisp = process_editscript(edscr); } disp=ndisp; term.innerHTML=ndisp; if (visible_height_frac != 1) { var termheight = visible_height_frac * term.scrollHeight; term.style.height = termheight+"px"; term.scrollTop = term.scrollHeight; } } function scrollterm(pages) { term.scrollTop += pages * visible_height_frac * term.scrollHeight; } var rcv_timeout; function get() { //alert("get"); rcv_loader.load(url_prefix+"anyterm-module","a=rcv&s="+session+cachebust()); rcv_timeout = window.setTimeout("alert('no response from server after 60 secs')",60000); } function rcv(resp) { // Called asynchronously when the received document has returned // from the server. window.clearTimeout(rcv_timeout); if (!open) { return; } if (resp=="") { // We seem to get this if the connection to the server fails. alert("Connection to server failed"); return; } if (handle_resp_error(resp)) { return; } display(resp); get(); } rcv_loader = new AsyncLoader(rcv); // Transmit channel: var kb_buf=""; var send_loader; var send_in_progress=false; function send() { send_in_progress=true; send_loader.load(url_prefix+"anyterm-module", "a=send&s="+session+cachebust()+"&k="+encodeURIComponent(kb_buf)); kb_buf=""; } function send_done(resp) { send_in_progress=false; if (handle_resp_error(resp)) { return; } if (kb_buf!="") { send(); } } send_loader = new AsyncLoader(send_done); function maybe_send() { if (!send_in_progress && open && kb_buf!="") { send(); } } function process_key(k) { // alert("key="+k); // return; kb_buf+=k; maybe_send(); } function esc_seq(s) { return String.fromCharCode(27)+"["+s; } function key_ev_stop(ev) { // We want this key event to do absolutely nothing else. ev.cancelBubble=true; if (ev.stopPropagation) ev.stopPropagation(); if (ev.preventDefault) ev.preventDefault(); try { ev.keyCode=0; } catch(e){} } function key_ev_supress(ev) { // We want this keydown event to become a keypress event, but nothing else. ev.cancelBubble=true; if (ev.stopPropagation) ev.stopPropagation(); } // When a key is pressed the browser delivers several events: typically first a keydown // event, then a keypress event, then a keyup event. Ideally we'd just use the keypress // event, but there's a problem with that: the browser may not send a keypress event for // unusual keys such as function keys, control keys, cursor keys and so on. The exact // behaviour varies between browsers and probably versions of browsers. // // So to get these keys we need to get the keydown events. They have a couple of // problems. Firstly, you get these events for things like pressing the shift key. // Secondly, unlike keypress events you don't get auto-repeat. function keypress(ev) { if (!ev) var ev=window.event; // Only handle "safe" characters here. Anything unusual is ignored; it would // have been handled earlier by the keydown function below. if ((ev.ctrlKey && !ev.altKey) // Ctrl is pressed (but not altgr, which is reported // as ctrl+alt in at least some browsers). || (ev.which==0) // there's no key in the event; maybe a shift key? // (Mozilla sends which==0 && keyCode==0 when you press // the 'windows logo' key.) || (ev.keyCode==8) // backspace || (ev.keyCode==16)) { // shift; Opera sends this. key_ev_stop(ev); return false; } var kc; if (ev.keyCode) kc=ev.keyCode; if (ev.which) kc=ev.which; var k=String.fromCharCode(kc); // When a key is pressed with ALT, we send ESC followed by the key's normal // code. But we don't want to do this when ALT-GR is pressed. if (ev.altKey && !ev.ctrlKey) { k = String.fromCharCode(27)+k; } // alert("keypress keyCode="+ev.keyCode+" which="+ev.which+ // " shiftKey="+ev.shiftKey+" ctrlKey="+ev.ctrlKey+" altKey="+ev.altKey); process_key(k); key_ev_stop(ev); return false; } function keydown(ev) { if (!ev) var ev=window.event; // alert("keydown keyCode="+ev.keyCode+" which="+ev.which+ // " shiftKey="+ev.shiftKey+" ctrlKey="+ev.ctrlKey+" altKey="+ev.altKey); var k; var kc=ev.keyCode; // Handle special keys. We do this here because IE doesn't send // keypress events for these (or at least some versions of IE don't for // at least many of them). This is unfortunate as it means that the // cursor keys don't auto-repeat, even in browsers where that would be // possible. That could be improved. // Interpret shift-pageup/down locally if (ev.shiftKey && kc==33) { scrollterm(-0.5); key_ev_stop(ev); return false; } else if (ev.shiftKey && kc==34) { scrollterm(0.5); key_ev_stop(ev); return false; } else if (kc==33) k=esc_seq("5~"); // PgUp else if (kc==34) k=esc_seq("6~"); // PgDn else if (kc==35) k=esc_seq("4~"); // End else if (kc==36) k=esc_seq("1~"); // Home else if (kc==37) k=esc_seq("D"); // Left else if (kc==38) k=esc_seq("A"); // Up else if (kc==39) k=esc_seq("C"); // Right else if (kc==40) k=esc_seq("B"); // Down else if (kc==45) k=esc_seq("2~"); // Ins else if (kc==46) k=esc_seq("3~"); // Del else if (kc==27) k=String.fromCharCode(27); // Escape else if (kc==9) k=String.fromCharCode(9); // Tab else if (kc==8) k=String.fromCharCode(8); // Backspace else if (kc==112) k=esc_seq(ev.shiftKey ? "25~" : "[A"); // F1 else if (kc==113) k=esc_seq(ev.shiftKey ? "26~" : "[B"); // F2 else if (kc==114) k=esc_seq(ev.shiftKey ? "28~" : "[C"); // F3 else if (kc==115) k=esc_seq(ev.shiftKey ? "29~" : "[D"); // F4 else if (kc==116) k=esc_seq(ev.shiftKey ? "31~" : "[E"); // F5 else if (kc==117) k=esc_seq(ev.shiftKey ? "32~" : "17~"); // F6 else if (kc==118) k=esc_seq(ev.shiftKey ? "33~" : "18~"); // F7 else if (kc==119) k=esc_seq(ev.shiftKey ? "34~" : "19~"); // F8 else if (kc==120) k=esc_seq("20~"); // F9 else if (kc==121) k=esc_seq("21~"); // F10 else if (kc==122) k=esc_seq("23~"); // F11 else if (kc==123) k=esc_seq("24~"); // F12 else { // For most keys we'll stop now and let the subsequent keypress event // process the key. This has the advantage that auto-repeat will work. // But we'll carry on here for control keys. // Note that when altgr is pressed, the event reports ctrl and alt being // pressed because it doesn't have a separate field for altgr. We'll // handle altgr in the keypress handler. if (!ev.ctrlKey // ctrl not pressed || (ev.ctrlKey && ev.altKey) // altgr pressed || (ev.keyCode==17)) { // I think that if you press shift-control, // you'll get an event with !ctrlKey && keyCode==17. key_ev_supress(ev); return; // Note that we don't "return false" here, as we want the // keypress handler to be invoked. } // OK, so now we're handling a ctrl key combination. // There are some assumptions below about whether these symbols are shifted // or not; does this work with different keyboards? if (ev.shiftKey) { if (kc==50) k=String.fromCharCode(0); // Ctrl-@ else if (kc==54) k=String.fromCharCode(30); // Ctrl-^, doesn't work else if (kc==94) k=String.fromCharCode(30); // Ctrl-^, doesn't work else if (kc==109) k=String.fromCharCode(31); // Ctrl-_ else { key_ev_supress(ev); return; } } else { if (kc>=65 && kc<=90) k=String.fromCharCode(kc-64); // Ctrl-A..Z else if (kc==219) k=String.fromCharCode(27); // Ctrl-[ else if (kc==220) k=String.fromCharCode(28); // Ctrl-\ . else if (kc==221) k=String.fromCharCode(29); // Ctrl-] else if (kc==190) k=String.fromCharCode(30); // Since ctrl-^ doesn't work, map // ctrl-. to its code. else if (kc==32) k=String.fromCharCode(0); // Ctrl-space sends 0, like ctrl-@. else { key_ev_supress(ev); return; } } } // alert("keydown keyCode="+ev.keyCode+" which="+ev.which+ // " shiftKey="+ev.shiftKey+" ctrlKey="+ev.ctrlKey+" altKey="+ev.altKey); process_key(k); key_ev_stop(ev); return false; } // Open, close and initialisation: function open_term(rows,cols,p,charset,scrollback) { var params = "a=open&rows="+rows+"&cols="+cols; if (p) { params += "&p="+p; } if (charset) { params += "&ch="+charset; } if (scrollback) { if (scrollback>1000) { alert("The maximum scrollback is currently limited to 1000 lines. " +"Please choose a smaller value and try again."); return; } params += "&sb="+scrollback; } params += cachebust(); var resp = sync_load(url_prefix+"anyterm-module",params); if (handle_resp_error(resp)) { return; } open=true; session=resp; } function close_term() { if (!open) { alert("Connection is not open"); return; } open=false; var resp = sync_load(url_prefix+"anyterm-module","a=close&s="+session+cachebust()); handle_resp_error(resp); // If we get an error, we still close everything. document.onkeypress=null; document.onkeydown=null; window.onbeforeunload=null; var e; while (e=frame.firstChild) { frame.removeChild(e); } frame.className=""; if (on_close_goto_url) { document.location = on_close_goto_url; } } function get_anyterm_version() { var svn_url="$URL: file:///var/lib/svn/anyterm/tags/releases/1.1/1.1.29/browser/anyterm.js $"; var re = /releases\/[0-9]+\.[0-9]+\/([0-9\.]+)/; var match = re.exec(svn_url); if (match) { return match[1]; } else { return ""; } } function substitute_variables(s) { var version = get_anyterm_version(); if (version!="") { version="-"+version; } var hostname=document.location.host; return s.replace(/%v/g,version).replace(/%h/g,hostname); } // Copying function copy_ie_clipboard() { try { window.document.execCommand("copy",false,null); } catch (err) { return undefined; } return 1; } function copy_mozilla_clipboard() { // Thanks to Simon Wissinger for this function. try { netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); } catch (err) { return undefined; } var sel=window.getSelection(); var copytext=sel.toString(); var str=Components.classes["@mozilla.org/supports-string;1"] .createInstance(Components.interfaces.nsISupportsString); if (!str) return undefined; str.data=copytext; var trans=Components.classes["@mozilla.org/widget/transferable;1"] .createInstance(Components.interfaces.nsITransferable); if (!trans) return undefined; trans.addDataFlavor("text/unicode"); trans.setTransferData("text/unicode", str, copytext.length * 2); var clipid=Components.interfaces.nsIClipboard; var clip=Components.classes["@mozilla.org/widget/clipboard;1"].getService(clipid); if (!clip) return undefined; clip.setData(trans, null, clipid.kGlobalClipboard); return 1; } function copy_to_clipboard() { var r=copy_ie_clipboard(); if (r==undefined) { r=copy_mozilla_clipboard(); } if (r==undefined) { alert("Copy seems to be disabled; maybe you need to change your security settings?" +"\n(Copy on the Edit menu will probably work)"); } } // Pasting function get_mozilla_clipboard() { // This function is taken from // http://www.nomorepasting.com/paste.php?action=getpaste&pasteID=41974&PHPSESSID=e6565dcf5de07256345e562b97ac9f46 // which does not indicate any particular copyright conditions. It // is a public forum, so one might conclude that it is public // domain. // IMHO it's disgraceful that Mozilla makes us use these 30 lines of // undocumented gobledegook to do what IE does, and documents, with // just 'window.clipboardData.getData("Text")'. What on earth were // they thinking? try { netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); } catch (err) { return undefined; } var clip = Components.classes["@mozilla.org/widget/clipboard;1"] .createInstance(Components.interfaces.nsIClipboard); if (!clip) { return undefined; } var trans = Components.classes["@mozilla.org/widget/transferable;1"] .createInstance(Components.interfaces.nsITransferable); if (!trans) { return undefined; } trans.addDataFlavor("text/unicode"); clip.getData(trans,clip.kGlobalClipboard); var str=new Object(); var strLength=new Object(); try { trans.getTransferData("text/unicode",str,strLength); } catch(err) { // One reason for getting here seems to be that nothing is selected return ""; } if (str) { str=str.value.QueryInterface(Components.interfaces.nsISupportsString); } if (str) { return str.data.substring(0,strLength.value / 2); } else { return ""; // ? is this "clipboard empty" or "cannot access"? } } function get_ie_clipboard() { if (window.clipboardData) { return window.clipboardData.getData("Text"); } return undefined; } function get_default_clipboard() { return prompt("Paste into this box and press OK:",""); } function paste_from_clipboard() { var p = get_ie_clipboard(); if (p==undefined) { p = get_mozilla_clipboard(); } if (p==undefined) { p = get_default_clipboard(); if (p) { process_key(p); } return; } if (p=="") { alert("The clipboard seems to be empty"); return; } if (confirm('Click OK to "type" the following into the terminal:\n'+p)) { process_key(p); } } function create_button(label,fn) { var button=document.createElement("A"); var button_t=document.createTextNode("["+label+"] "); button.appendChild(button_t); button.onclick=fn; return button; } function create_img_button(imgfn,label,fn) { var button=document.createElement("A"); var button_img=document.createElement("IMG"); var class_attr=document.createAttribute("CLASS"); class_attr.value="button"; button_img.setAttributeNode(class_attr); var src_attr=document.createAttribute("SRC"); src_attr.value=imgfn; button_img.setAttributeNode(src_attr); var alt_attr=document.createAttribute("ALT"); alt_attr.value="["+label+"] "; button_img.setAttributeNode(alt_attr); var title_attr=document.createAttribute("TITLE"); title_attr.value=label; button_img.setAttributeNode(title_attr); button.appendChild(button_img); button.onclick=fn; return button; } function create_term(elem_id,title,rows,cols,p,charset,scrollback) { if (open) { alert("Terminal is already open"); return; } title=substitute_variables(title); frame=document.getElementById(elem_id); if (!frame) { alert("There is no element named '"+elem_id+"' in which to build a terminal"); return; } frame.className="termframe"; var title_p=document.createElement("P"); title_p.appendChild(create_img_button("copy.gif","Copy",copy_to_clipboard)); title_p.appendChild(create_img_button("paste.gif","Paste",paste_from_clipboard)); title_p.appendChild(create_ctrlkey_menu()); var title_t=document.createTextNode(" "+title+" "); title_p.appendChild(title_t); // title_p.appendChild(create_button("close",close_term)); frame.appendChild(title_p); term=document.createElement("PRE"); frame.appendChild(term); term.className="term a p"; var termbody=document.createTextNode(""); term.appendChild(termbody); visible_height_frac=Number(rows)/(Number(rows)+Number(scrollback)); if (scrollback>0) { term.style.overflowY="scroll"; } document.onhelp = function() { return false; }; document.onkeypress=keypress; document.onkeydown=keydown; open_term(rows,cols,p,charset,scrollback); if (open) { window.onbeforeunload=warn_unload; get(); maybe_send(); } } function warn_unload() { if (open) { return "Leaving this page will close the terminal."; } } function create_ctrlkey_menu() { var sel=document.createElement("SELECT"); create_ctrlkey_menu_entry(sel,"Control keys...",-1); create_ctrlkey_menu_entry(sel,"Ctrl-@",0); for (var code=1; code<27; code++) { var letter=String.fromCharCode(64+code); create_ctrlkey_menu_entry(sel,"Ctrl-"+letter,code); } create_ctrlkey_menu_entry(sel,"Ctrl-[",27); create_ctrlkey_menu_entry(sel,"Ctrl-\\",28); create_ctrlkey_menu_entry(sel,"Ctrl-]",29); create_ctrlkey_menu_entry(sel,"Ctrl-^",30); create_ctrlkey_menu_entry(sel,"Ctrl-_",31); sel.onchange=function() { var code = sel.options[sel.selectedIndex].value; if (code>=0) { process_key(String.fromCharCode(code)); } }; return sel; } function create_ctrlkey_menu_entry(sel,name,code) { var opt=document.createElement("OPTION"); opt.appendChild(document.createTextNode(name)); var value_attr=document.createAttribute("VALUE"); value_attr.value=code; opt.setAttributeNode(value_attr); sel.appendChild(opt); }