├── .babelrc ├── .gitignore ├── Dockerfile ├── archive ├── WebTerminal-0.4-stable.xml ├── WebTerminal-0.6-dev.xml ├── WebTerminal-0.6.7-stable.xml ├── WebTerminal-0.85-dev.xml ├── WebTerminal-0.89-dev.xml ├── WebTerminal-0.9-dev.xml ├── WebTerminal-0.9.1-stable.xml ├── WebTerminal-0.9.5-stable.xml ├── WebTerminal-0.9.6-stable.xml ├── WebTerminal-0.9.6.8-dev.xml ├── WebTerminal-0.9.7-stable.xml ├── WebTerminal-0.9.7.8-dev.xml ├── WebTerminal-0.9.9-stable.xml ├── WebTerminal-0.9.9.5-dev.xml ├── WebTerminal-0.9.9.7-dev.xml ├── WebTerminal-0.9.9.8-dev.xml ├── WebTerminal-2.0.0-alpha.1.xml ├── WebTerminal-2.0.0-beta.1.xml ├── WebTerminal-2.0.0-beta.2.xml ├── WebTerminal-2.0.0-beta.3.xml ├── WebTerminal-2.0.0-beta.4.xml ├── WebTerminal-2.0.0-beta.5.xml ├── WebTerminal-2.0.0-beta.6.xml └── WebTerminal-2.0.0-beta.xml ├── build └── cls │ └── WebTerminal │ ├── Analytics.cls │ ├── Autocomplete.cls │ ├── Common.cls │ ├── Core.cls │ ├── Engine.cls │ ├── ErrorDecomposer.cls │ ├── Handlers.cls │ ├── Installer.cls │ ├── Router.cls │ ├── StaticContent.cls │ ├── Trace.cls │ └── Updater.cls ├── contributing.md ├── docker-compose.yml ├── docs ├── css │ ├── content.css │ └── main.css ├── favicon.ico ├── files │ ├── CacheWebTerminal-v3.1.4.xml │ ├── CacheWebTerminal-v3.1.5.xml │ ├── CacheWebTerminal-v3.2.0.xml │ ├── CacheWebTerminal-v3.2.2.xml │ ├── CacheWebTerminal-v3.2.5.xml │ ├── CacheWebTerminal-v3.2.7.xml │ ├── CacheWebTerminal-v3.3.0.xml │ ├── CacheWebTerminal-v3.3.1.xml │ ├── WebTerminal-0.9.9.9-dev.xml │ ├── WebTerminal-1.0-stable-beta.xml │ ├── WebTerminal-2.0.0-beta.7.xml │ ├── WebTerminal-2.0.0-beta.8.xml │ ├── WebTerminal-3.0.0-alpha.5.xml │ ├── WebTerminal-3.0.0-alpha.6.xml │ ├── WebTerminal-3.1.3.xml │ ├── WebTerminal-v4.0.0-alpha.10.xml │ ├── WebTerminal-v4.0.0-alpha.15.xml │ ├── WebTerminal-v4.0.0-alpha.2.xml │ ├── WebTerminal-v4.0.0-alpha.26.xml │ ├── WebTerminal-v4.0.0-alpha.3.xml │ ├── WebTerminal-v4.0.0-alpha.31.xml │ ├── WebTerminal-v4.0.0-alpha.33.xml │ ├── WebTerminal-v4.0.0-alpha.4.xml │ ├── WebTerminal-v4.0.0-alpha.41.xml │ ├── WebTerminal-v4.0.0-alpha.44.xml │ ├── WebTerminal-v4.0.0-alpha.5.xml │ ├── WebTerminal-v4.0.0-alpha.50.xml │ ├── WebTerminal-v4.0.0-alpha.51-broken.xml │ ├── WebTerminal-v4.0.0-alpha.53.xml │ ├── WebTerminal-v4.0.0-alpha.55.xml │ ├── WebTerminal-v4.0.0-alpha.57.xml │ ├── WebTerminal-v4.0.0-alpha.58.xml │ ├── WebTerminal-v4.0.0-alpha.66.xml │ ├── WebTerminal-v4.0.0-alpha.68.xml │ ├── WebTerminal-v4.0.0-alpha.71.xml │ ├── WebTerminal-v4.0.0-alpha.73.xml │ ├── WebTerminal-v4.0.0-alpha.75.xml │ ├── WebTerminal-v4.0.0-alpha.8.xml │ ├── WebTerminal-v4.0.0-beta.1.xml │ ├── WebTerminal-v4.0.0-beta.11.xml │ ├── WebTerminal-v4.0.0-beta.12.xml │ ├── WebTerminal-v4.0.0-beta.14.xml │ ├── WebTerminal-v4.0.0-beta.16.xml │ ├── WebTerminal-v4.0.0-beta.17.xml │ ├── WebTerminal-v4.0.0-beta.3.xml │ ├── WebTerminal-v4.0.0-beta.4.xml │ ├── WebTerminal-v4.0.0-beta.5.xml │ ├── WebTerminal-v4.0.0-beta.6.xml │ ├── WebTerminal-v4.0.0-beta.7.xml │ ├── WebTerminal-v4.0.0-beta.8.xml │ ├── WebTerminal-v4.0.0.xml │ ├── WebTerminal-v4.1.1.xml │ ├── WebTerminal-v4.1.2.xml │ ├── WebTerminal-v4.2.0.xml │ ├── WebTerminal-v4.2.12.xml │ ├── WebTerminal-v4.2.13.xml │ ├── WebTerminal-v4.2.14.xml │ ├── WebTerminal-v4.2.15.xml │ ├── WebTerminal-v4.2.2.xml │ ├── WebTerminal-v4.2.3.xml │ ├── WebTerminal-v4.2.6.xml │ ├── WebTerminal-v4.2.8.xml │ ├── WebTerminal-v4.3.0.xml │ ├── WebTerminal-v4.3.1.xml │ ├── WebTerminal-v4.4.1.xml │ ├── WebTerminal-v4.5.0.xml │ ├── WebTerminal-v4.6.0.xml │ ├── WebTerminal-v4.6.1.xml │ ├── WebTerminal-v4.6.3.xml │ ├── WebTerminal-v4.7.0.xml │ ├── WebTerminal-v4.7.3.xml │ ├── WebTerminal-v4.7.4.xml │ ├── WebTerminal-v4.8.0.xml │ ├── WebTerminal-v4.8.3.xml │ ├── WebTerminal-v4.9.0.xml │ ├── WebTerminal-v4.9.1.xml │ ├── WebTerminal-v4.9.2.xml │ ├── WebTerminal-v4.9.3.xml │ ├── WebTerminal-v4.9.4.xml │ └── WebTerminal-v4.9.5.xml ├── img │ ├── WebTerminal Preview.png │ ├── back.png │ ├── fork.png │ ├── icons │ │ ├── docs.png │ │ ├── download.png │ │ ├── feedback.png │ │ ├── images.png │ │ ├── save.png │ │ └── terminal.png │ ├── logos.png │ └── screenshoots │ │ ├── ac.png │ │ ├── features-config.png │ │ ├── features-help.png │ │ ├── features-mobile.png │ │ ├── install-steps.png │ │ ├── scr4.png │ │ └── showcase-slq-mode.png ├── index.html ├── latestNightVersion ├── latestVersion ├── readme.md └── terminal.json ├── gulpfile.babel.js ├── import.bat ├── iris.script ├── issue_template.md ├── license ├── module.xml ├── package.json ├── readme.md └── src ├── client ├── index.html ├── js │ ├── analytics │ │ └── index.js │ ├── autocomplete │ │ ├── hint.js │ │ ├── index.js │ │ └── types.js │ ├── config.js │ ├── elements.js │ ├── favorite.js │ ├── index.js │ ├── init.js │ ├── input │ │ ├── caret.js │ │ ├── handlers.js │ │ ├── history.js │ │ ├── index.js │ │ └── special.js │ ├── lib.js │ ├── localization │ │ ├── dictionary.js │ │ └── index.js │ ├── network │ │ ├── index.js │ │ └── update.js │ ├── output │ │ ├── Line.js │ │ ├── const.js │ │ ├── esc.js │ │ ├── escStateMachine.js │ │ └── index.js │ ├── parser │ │ ├── _build.js │ │ ├── grammar.js │ │ ├── index.js │ │ └── pushdownAutomaton.js │ ├── server │ │ ├── handlers.js │ │ └── index.js │ ├── settings.js │ ├── storage.js │ └── tracing │ │ └── index.js └── scss │ ├── graphic.scss │ ├── hintBox.scss │ ├── index.scss │ ├── mixins.scss │ ├── terminal.scss │ └── themes │ └── cache.scss ├── cls └── WebTerminal │ ├── Analytics.cls │ ├── Autocomplete.cls │ ├── Common.cls │ ├── Core.cls │ ├── Engine.cls │ ├── ErrorDecomposer.cls │ ├── Handlers.cls │ ├── Installer.cls │ ├── Router.cls │ ├── StaticContent.cls │ ├── Trace.cls │ └── Updater.cls └── mac └── WebTerminal └── EscapeSequencesTest.mac /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | build/ 3 | node_modules/ 4 | package-lock.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG IMAGE=intersystemsdc/iris-ml-community:latest as build 2 | FROM $IMAGE 3 | 4 | USER root 5 | 6 | WORKDIR /opt/irisapp 7 | RUN chown ${ISC_PACKAGE_MGRUSER}:${ISC_PACKAGE_IRISGROUP} /opt/irisapp 8 | USER ${ISC_PACKAGE_MGRUSER} 9 | 10 | COPY build build 11 | COPY module.xml module.xml 12 | COPY iris.script /tmp/iris.script 13 | 14 | RUN iris start IRIS \ 15 | && iris session IRIS < /tmp/iris.script \ 16 | && iris stop IRIS quietly 17 | -------------------------------------------------------------------------------- /build/cls/WebTerminal/Analytics.cls: -------------------------------------------------------------------------------- 1 | /// This class includes methods which collect WebTerminal's analytics such as error and installation reports. 2 | Class WebTerminal.Analytics 3 | { 4 | 5 | /// This method sends a report about installation status, including error message if any errors happened. 6 | ClassMethod ReportInstallStatus(status As %Status = 1, type As %String = "Install") As %Status 7 | { 8 | set req = ##class(%Net.HttpRequest).%New() 9 | set req.Server = "www.google-analytics.com" 10 | do req.EntityBody.Write("v=1&tid=UA-83005064-2&cid="_##class(%SYS.System).InstanceGUID() 11 | _"&ds=web&an=WebTerminal&av="_##class(WebTerminal.Installer).#VERSION 12 | _"&t=event&aiid="_$ZCONVERT($zv, "O", "URL")_"&ec="_$ZCONVERT(type, "O", "URL")_"&ea=" 13 | _$case($$$ISOK(status), 1: "Success", : "Failure")_"&el=" 14 | _$ZCONVERT($System.Status.GetErrorText(status), "O", "URL")) 15 | try { 16 | return req.Post("/collect") 17 | } catch e { 18 | write "Unable to send analytics to " _ req.Server _ ", skipping analytics collection." 19 | return $$$OK 20 | } 21 | } 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /build/cls/WebTerminal/Autocomplete.cls: -------------------------------------------------------------------------------- 1 | Class WebTerminal.Autocomplete Extends Common 2 | { 3 | 4 | /// Returns a comma-delimited string of globals names in the namespace, which begin from "beginning". 5 | ClassMethod GetGlobals(namespace As %String = "%SYS", beginning As %String = "") As %String 6 | { 7 | set result = "" 8 | set pattern = beginning _ "*" 9 | new $Namespace 10 | set $Namespace = namespace 11 | set rset = ##class(%ResultSet).%New("%SYS.GlobalQuery:NameSpaceList") 12 | do rset.Execute($Namespace, pattern, 1) 13 | while (rset.Next()) { 14 | set result = result _ $case(result = "", 1:"", :",") _ rset.GetData(1) 15 | } 16 | return result 17 | } 18 | 19 | /// Returns a comma-delimited string of class names in the namespace, which begin from "beginning". 20 | ClassMethod GetClass(namespace As %String = "%SYS", beginning As %String = "") As %String 21 | { 22 | new $Namespace 23 | set $Namespace = namespace 24 | set pattern = $REPLACE(beginning, "%", "!%") _ "%" 25 | &sql(select LIST(ID) into :ids from %Dictionary.CompiledClass where ID like :pattern ESCAPE '!' and deployed <> 2) 26 | return ids 27 | } 28 | 29 | /// Returns a comma-delimited string of public class members (accessible through ##class() construction) in the class of namespace. 30 | ClassMethod GetPublicClassMembers(namespace As %String = "%SYS", className As %String = "", beginning As %String = "") As %String 31 | { 32 | new $Namespace 33 | set $Namespace = namespace 34 | set pattern = $REPLACE(beginning, "%", "!%") _ "%" 35 | &sql(select LIST(Name) into :names from %Dictionary.CompiledMethod WHERE parent=:className AND ClassMethod=1 AND Name like :pattern ESCAPE '!') 36 | return names 37 | } 38 | 39 | /// Returns a comma-delimited string of class members in the class of namespace. 40 | ClassMethod GetClassMembers(namespace As %String = "%SYS", className As %String = "", beginning As %String = "", methodsOnly = "") As %String 41 | { 42 | new $Namespace 43 | set $Namespace = namespace 44 | if $EXTRACT(beginning, 1) = "#" { 45 | set ps = ..GetParameters(namespace, className, $EXTRACT(beginning, 2, $LENGTH(beginning))) 46 | return:(ps = "") ps 47 | return "#"_$REPLACE(ps, ",", ",#") 48 | } 49 | set pattern = $REPLACE(beginning, "%", "!%") _ "%" 50 | set props = "" 51 | &sql(select LIST(Name) into :methods from %Dictionary.CompiledMethod WHERE parent=:className AND Private = 0 AND Name like :pattern ESCAPE '!') 52 | if (methodsOnly = "") { 53 | &sql(select LIST(Name) into :props from %Dictionary.CompiledProperty WHERE parent=:className AND Name like :pattern ESCAPE '!') 54 | } 55 | return $case((methods '= "") && (props '= ""), 1: methods _ "," _ props, : methods _ props) 56 | } 57 | 58 | /// Returns a comma-delimited string of class members in the class of namespace. 59 | ClassMethod GetParameters(namespace As %String = "%SYS", className As %String = "", beginning As %String = "") As %String 60 | { 61 | new $Namespace 62 | set $Namespace = namespace 63 | set pattern = $REPLACE(beginning, "%", "!%") _ "%" 64 | &sql(select LIST(Name) into :names from %Dictionary.CompiledParameter WHERE parent=:className AND Name like :pattern ESCAPE '!') 65 | return names 66 | } 67 | 68 | /// Returns a comma-delimited string of routine names in the namespace, which begin from "beginning". 69 | ClassMethod GetRoutines(namespace As %String = "%SYS", beginning As %String = "") As %String 70 | { 71 | set result = "" 72 | set pattern = beginning _ "*.*" 73 | new $Namespace 74 | set $Namespace = namespace 75 | set rset = ##class(%ResultSet).%New("%Library.Routine:RoutineList") 76 | do rset.Execute(pattern, , , $Namespace) 77 | while (rset.Next()) { 78 | set result = result _ $case(result = "", 1:"", :",") _ $PIECE(rset.GetData(1), ".", 1, *-1) 79 | } 80 | return result 81 | } 82 | 83 | } 84 | 85 | -------------------------------------------------------------------------------- /build/cls/WebTerminal/Common.cls: -------------------------------------------------------------------------------- 1 | Include %sySystem 2 | 3 | Class WebTerminal.Common 4 | { 5 | 6 | /// Interprocess communication cannot handle big messages at once, so they need to be split. 7 | Parameter ChunkSize = 45; 8 | 9 | /// Send the chunk of data to another process. The process need to receive the chunk with the 10 | /// appropriate function ReceiveChunk. Consider event length less than 44 characters long. 11 | ClassMethod SendChunk(pid As %Numeric, flag As %String, data As %String = "") As %Status 12 | { 13 | set pos = 1 14 | set len = $LENGTH(data) + 1 // send the last empty message if the data size = ChunkSize 15 | for { 16 | try { 17 | set st = $system.Event.Signal( 18 | pid, 19 | $LB(flag, $EXTRACT(data, pos, pos + ..#ChunkSize - 1)) 20 | ) 21 | } catch (e) { return $$$NOTOK } 22 | if (st '= 1) { return $$$NOTOK } 23 | set pos = pos + ..#ChunkSize 24 | if (pos > len) { quit } 25 | } 26 | return $$$OK 27 | } 28 | 29 | /// Receives the chunk of data from another process. Returns the $LISTBUILD string which contains 30 | /// flag at the first position and string at the second. This method also terminates the process 31 | /// if the parent process is gone. 32 | ClassMethod ReceiveChunk(timeout As %Numeric = -1, masterProcess = 0) As %String 33 | { 34 | set flag = "" 35 | set str = "" 36 | set status = -1 37 | for { 38 | set message = $system.Event.WaitMsg("", $Case(timeout = -1, 1: 1, :timeout)) 39 | set status = $LISTGET(message, 1) 40 | set data = $LISTGET(message, 2) 41 | if (status <= 0) { 42 | if ($ZPARENT '= 0) && ('$data(^$Job($ZPARENT))) { 43 | do $system.Process.Terminate($JOB, 0) 44 | return $LISTBUILD("e", $LISTBUILD("", "Parent process "_$JOB_" is gone"), -1) 45 | } 46 | if masterProcess && ($ZCHILD '= 0) && ('$data(^$Job($ZCHILD))) { 47 | return $LISTBUILD("e", $LISTBUILD("", "Child process "_$ZCHILD_" is gone"), -1) 48 | } 49 | } 50 | if (data = "") && (timeout = 0) quit 51 | if (status <= 0) { 52 | set:(timeout = 0) timeout = 1 53 | continue 54 | } 55 | set flag = $LISTGET(data, 1) 56 | set m = $LISTGET(data, 2) 57 | set str = str _ m 58 | if (timeout = 0) set timeout = 1 59 | quit:($LENGTH(m) '= ..#ChunkSize) 60 | } 61 | return $LISTBUILD(flag, str, status) 62 | } 63 | 64 | /// Returns the contents of the proxy object to the current device in JSON format.
65 | /// This method is called when a proxy object is used in conjunction with 66 | /// the %ZEN.Auxiliary.jsonProvider component.
67 | /// format is a flags string to control output formatting options.
68 | /// The following character option codes are supported:
69 | /// 1-9 : indent with this number of spaces (4 is the default with the 'i' format specifier)
70 | /// a - output null arrays/objects
71 | /// b - line break before opening { of objects
72 | /// c - output the Caché-specific "_class" and "_id" properties (if a child property is an instance of a concrete object class)
73 | /// e - output empty object properties
74 | /// i - indent with 4 spaces unless 't' or 1-9
75 | /// l - output empty lists
76 | /// n - newline (lf)
77 | /// o - output empty arrays/objects
78 | /// q - output numeric values unquoted even when they come from a non-numeric property
79 | /// s - use strict JSON output - NOTE: special care should be taken when sending data to a browser, as using this flag 80 | /// may expose you to cross site scripting (XSS) vulnerabilities if the data is sent inside <script> tags. Zen uses 81 | /// this technique extensively, so this flag should NOT be specified for jsonProviders in Zen pages.
82 | /// t - indent with tab character
83 | /// u - output pre-converted to UTF-8 instead of in native internal format
84 | /// w - Windows-style cr/lf newline
85 | ClassMethod GetJSONString(obj As %ZEN.proxyObject, format As %String = "aeos") As %String [ ProcedureBlock = 0 ] 86 | { 87 | set tOldIORedirected = ##class(%Device).ReDirectIO() 88 | set tOldMnemonic = ##class(%Device).GetMnemonicRoutine() 89 | set tOldIO = $io 90 | try { 91 | set str = "" 92 | use $io::("^" _ $ZNAME) 93 | do ##class(%Device).ReDirectIO(1) 94 | do ##class(%ZEN.Auxiliary.jsonProvider).%ObjectToJSON(obj,,,format) 95 | } catch ex { 96 | set str = "" 97 | } 98 | if (tOldMnemonic '= "") { 99 | use tOldIO::("^" _ tOldMnemonic) 100 | } else { 101 | use tOldIO 102 | } 103 | do ##class(%Device).ReDirectIO(tOldIORedirected) 104 | return str 105 | 106 | rchr(c) 107 | quit 108 | rstr(sz,to) 109 | quit 110 | wchr(s) 111 | do output($char(s)) 112 | quit 113 | wff() 114 | do output($char(12)) 115 | quit 116 | wnl() 117 | do output($char(13,10)) 118 | quit 119 | wstr(s) 120 | do output(s) 121 | quit 122 | wtab(s) 123 | do output($char(9)) 124 | quit 125 | output(s) 126 | set str = str _ s 127 | quit 128 | } 129 | 130 | } 131 | 132 | -------------------------------------------------------------------------------- /build/cls/WebTerminal/Engine.cls: -------------------------------------------------------------------------------- 1 | /// Web Terminal version 4.9.5 WebSocket client. 2 | /// This class represents a connected client via WebSocket. 3 | Class WebTerminal.Engine Extends (%CSP.WebSocket, Common, Trace, Autocomplete) 4 | { 5 | 6 | /// Timeout in minutes when connection key expires. 7 | Parameter WSKEYEXPIRES = 3600; 8 | 9 | /// How long to wait for authorization key when connection established 10 | Parameter AuthorizationTimeout = 5; 11 | 12 | Property CurrentNamespace As %String; 13 | 14 | Property InitialZName As %String; 15 | 16 | Property InitialZNamespace As %String; 17 | 18 | /// The process ID of the terminal core. 19 | Property corePID As %Numeric [ InitialExpression = 0 ]; 20 | 21 | /// The last known namespace in child process. 22 | Property childNamespace As %String; 23 | 24 | Property StartupRoutine As %String; 25 | 26 | /// Output flag 27 | Property echo As %Boolean [ InitialExpression = 1 ]; 28 | 29 | /// flag which enables output buffering 30 | Property bufferOutput As %Boolean [ InitialExpression = 0 ]; 31 | 32 | /// Used to buffer the output ("o" flag) when bufferOutput flag is set 33 | Property outputBuffer As %Stream.TmpCharacter [ InitialExpression = {##class(%Stream.TmpCharacter).%New()} ]; 34 | 35 | /// Output flag 36 | Property handler As %Boolean [ InitialExpression = 0, Private ]; 37 | 38 | Method GetMessage(timeout As %Integer = 86400) As %ZEN.proxyObject 39 | { 40 | #define err(%e, %s) if (%e '= $$$OK) { set obj = ##class(%ZEN.proxyObject).%New() set obj.error = %s return obj } 41 | set data = ..Read(, .st, timeout) 42 | return:((st = $$$CSPWebSocketTimeout) || (st = $$$CSPWebSocketClosed)) "" 43 | $$$err(st, "%wsReadErr: "_$System.Status.GetErrorText(st)) 44 | set st = ##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(data, , .obj, 1) 45 | $$$err(st, "%wsParseErr: "_$System.Status.GetErrorText(st)) 46 | return obj 47 | } 48 | 49 | /// Do not remove this method in future versions of WebTerminal, it is used by update. 50 | Method Send(handler As %String = "", data = "") As %Status 51 | { 52 | if (handler = "o") && (..bufferOutput = 1) { 53 | do ..outputBuffer.Write(data) 54 | return $$$OK 55 | } 56 | return:((handler = "o") && (..echo = 0)) $$$OK 57 | return:(handler = "o") ..Write("o"_data) // Enables 2013.2 support (no JSON) 58 | set obj = ##class(%ZEN.proxyObject).%New() 59 | set obj.h = handler 60 | if (..handler '= 0) { 61 | set obj."_cb" = ..handler 62 | } 63 | set obj.d = data 64 | return ..Write(..GetJSONString(obj)) 65 | } 66 | 67 | Method OnPreServer() As %Status 68 | { 69 | set ..InitialZName = $zname 70 | set ..InitialZNamespace = $znspace 71 | quit $$$OK 72 | } 73 | 74 | Method OnPostServer() As %Status 75 | { 76 | if (..corePID '= 0) { 77 | do ..SendChunk(..corePID, "e") 78 | } 79 | quit $$$OK 80 | } 81 | 82 | ClassMethod WriteToFile(filename As %String, data As %String) As %Status 83 | { 84 | set file=##class(%File).%New(filename) 85 | do file.Open("WSN") 86 | do file.WriteLine(data) 87 | do file.Close() 88 | } 89 | 90 | Method ExecuteSQL(query As %String = "") As %Status 91 | { 92 | set tStatement = ##class(%SQL.Statement).%New() 93 | set qStatus = tStatement.%Prepare(query) 94 | if qStatus'=1 { 95 | write $System.Status.DisplayError(qStatus) 96 | } else { 97 | set rset = tStatement.%Execute() 98 | do rset.%Display() 99 | } 100 | quit $$$OK 101 | } 102 | 103 | /// This method performs the authorization and login to WebTerminal. 104 | /// It returns a list with data (see Router.Auth method), which is used then to set up the 105 | /// initial values for the client. 106 | Method RequireAuthorization() As %List 107 | { 108 | set data = ..GetMessage(..#AuthorizationTimeout) 109 | return:(data = "") $LB("%wsReadErr") 110 | return:('$IsObject(data.d)) $LB($case(data.error = "", 1: "Unresolved WS message format", :data.error)) 111 | return:(data.d.key = "") $LB("Missing key") 112 | 113 | set authKey = data.d.key 114 | set key = $ORDER(^WebTerminal("AuthUser", "")) 115 | set list = "" 116 | while (key '= "") { 117 | set lb = $GET(^WebTerminal("AuthUser", key)) 118 | if ((lb '= "") && (key = authKey)) { 119 | set list = lb 120 | } 121 | set time = $LISTGET(lb, 2) 122 | if (time '= "") && ($System.SQL.DATEDIFF("s", time, $h) > ..#WSKEYEXPIRES) { 123 | kill ^WebTerminal("AuthUser", key) 124 | } 125 | set key = $ORDER(^WebTerminal("AuthUser", key)) 126 | } 127 | 128 | if (list = "") { // not found 129 | return $LB("Invalid key") 130 | } 131 | 132 | set username = $LISTGET(list, 1) 133 | set namespace = $LISTGET(list, 3) 134 | set ns = $Namespace 135 | 136 | znspace "%SYS" 137 | do ##class(Security.Users).Get(username, .userProps) 138 | znspace ns 139 | 140 | set namespace = $case(namespace, "":userProps("NameSpace"), :namespace) 141 | 142 | if ($get(userProps("Routine")) '= "") { 143 | set ..StartupRoutine = userProps("Routine") 144 | } 145 | 146 | if $get(userProps("Enabled")) '= 1 { 147 | return $LB("User " _ username _ " is not enabled in the system") 148 | } 149 | 150 | set $LIST(list, 3) = namespace 151 | set loginStatus = $System.Security.Login(username) 152 | 153 | if (loginStatus '= 1) { 154 | return $LB($System.Status.GetErrorText(loginStatus)) 155 | } 156 | 157 | return $LB("", list) 158 | } 159 | 160 | /// See WebTerminal.Handlers 161 | Method ProcessRequest(handler As %String, data) As %Status [ Private ] 162 | { 163 | try { 164 | return $CLASSMETHOD("WebTerminal.Handlers", handler, $this, data) 165 | } catch (e) { 166 | set ..echo = 1 167 | return e.AsSystemError() 168 | } 169 | } 170 | 171 | /// Main method for every new client. 172 | Method ClientLoop() As %Status [ Private ] 173 | { 174 | job ##class(WebTerminal.Core).Loop(..StartupRoutine):($NAMESPACE):5 175 | if ($TEST '= 1) { // $TEST=0 for JOB only when timeouted 176 | do ..Send("error", "%noJob") 177 | return $$$NOTOK 178 | } 179 | set ..corePID = $ZCHILD 180 | set ..childNamespace = $NAMESPACE 181 | if (..StartupRoutine = "") { 182 | do ..Send("prompt", ..childNamespace) 183 | } else { 184 | set message = ##class(%ZEN.proxyObject).%New() 185 | set status = $CLASSMETHOD("WebTerminal.Handlers", "Execute", $this, "", 1) 186 | goto loopEnd 187 | } 188 | //try { // temp 189 | for { 190 | set message = ..GetMessage() 191 | quit:(message = "") // if client is gone, finish looping 192 | if (message.error '= "") { 193 | if (message.error '[ "ERROR #7951") { // don't try and send message if it was a WS close error 194 | set st = ..Send("error", message.error) 195 | } 196 | quit 197 | } 198 | if (message."_cb" '= "") { set ..handler = message."_cb" } 199 | set status = ..ProcessRequest(message.h, message.d) 200 | set ..handler = 0 201 | set ..echo = 1 202 | if (status '= "") && (status '= $$$OK) { 203 | set eType = $EXTRACT(status, 1, 1) 204 | do ..Send("oLocalized", $C(13,10) _ $case(eType = 0, 1: $System.Status.GetErrorText(status), :status)) 205 | continue 206 | } 207 | } 208 | loopEnd 209 | //} catch (e) { do ..Send("o", $System.Status.GetErrorText(e)) } // temp 210 | return $$$OK 211 | } 212 | 213 | /// This method sends basic login info to the user. Use this method to set client variables 214 | /// during the WebTerminal initialization. 215 | /// authList See Router.Auth method. 216 | Method SendLoginInfo(authList As %List) 217 | { 218 | set obj = ##class(%ZEN.proxyObject).%New() 219 | set obj.username = $USERNAME 220 | set obj.name = $get(^WebTerminal("Name")) 221 | set obj.cleanStart = $ListGet(authList, 4) 222 | set obj.system = $SYSTEM 223 | set obj.firstLaunch = ($get(^WebTerminal("FirstLaunch"), 1) '= 0) 224 | set obj.InstanceGUID = ##class(%SYS.System).InstanceGUID() 225 | set obj.zv = $ZVersion 226 | set ^WebTerminal("FirstLaunch") = 0 227 | do ..Send("init", obj) 228 | } 229 | 230 | /// Triggered when new connection established. 231 | Method Server() As %Status 232 | { 233 | set authRes = ..RequireAuthorization() 234 | set authMessage = $ListGet(authRes, 1) 235 | if (authMessage = "") { 236 | set authList = $ListGet(authRes, 2) // see Router.Auth method 237 | set namespace = $ListGet(authList, 3) 238 | if (namespace '= "") && (namespace '= $Namespace) { 239 | try { 240 | znspace namespace 241 | } catch (e) { 242 | do ..Send("oLocalized", 243 | $Char(27) _ "[31m%unNS(" _ namespace _ ")"_ $Char(27) _ "[0m" _ $Char(13,10) 244 | ) 245 | } 246 | } 247 | set ..CurrentNamespace = $Namespace 248 | do ..SendLoginInfo(authList) 249 | do ..ClientLoop() 250 | do ..Send("oLocalized", "%wsNormalClose"_$C(13,10)) 251 | } else { 252 | do ..Send("oLocalized", "%wsRefuse(" _ authMessage _ ")") 253 | } 254 | do ..EndServer() 255 | set %session.EndSession = 1 256 | quit $$$OK 257 | } 258 | 259 | } 260 | 261 | -------------------------------------------------------------------------------- /build/cls/WebTerminal/ErrorDecomposer.cls: -------------------------------------------------------------------------------- 1 | Class WebTerminal.ErrorDecomposer 2 | { 3 | 4 | Parameter LINES As %Numeric = 5; 5 | 6 | /// Takes $ZERROR function result. 7 | /// Returns either simple string or %ZEN.proxyObject representing the error details. 8 | ClassMethod DecomposeError(err As %String = "", ns As %String = "") 9 | { 10 | new $namespace 11 | if (ns '= "") { 12 | try { 13 | set $namespace = ns 14 | } catch (e) { 15 | return err 16 | } 17 | } 18 | return:($FIND(err, "<") '= 2) err 19 | set startPos = $FIND(err, ">") 20 | return:(startPos = 0) err 21 | set spacePos = $FIND(err, " ") - 1 22 | return:(spacePos = startPos) err 23 | set label = $EXTRACT(err, startPos, $case(spacePos = -1, 1:999, :spacePos-1)) 24 | return:(label = "") err 25 | try { 26 | set obj = ##class(%ZEN.proxyObject).%New() 27 | set obj.zerror = err 28 | set plusPos = $FIND(label, "+") 29 | set cPos = $FIND(label, "^") 30 | if (plusPos = 0) || (cPos = 0) { 31 | set obj.source = $TEXT(@label) 32 | set obj.line = 0 33 | return obj 34 | } 35 | set line = +$EXTRACT(label, plusPos, cPos - 2) 36 | set part1 = $EXTRACT(label, 1, plusPos - 1) 37 | set part2 = $EXTRACT(label, cPos - 1, *) 38 | set range = ..#LINES \ 2 39 | set obj.source = "" 40 | set obj.line = 0 41 | for i=line-range:1:line+range { 42 | continue:(i < 1) 43 | set label = part1 _ i _ part2 44 | set text = $TEXT(@label) 45 | set:(text '= "") obj.source = obj.source _ $case(obj.source = "", 1: "", :$C(10)) _ text 46 | set:((text '= "") && (i < line)) obj.line = obj.line + 1 47 | } 48 | return obj 49 | } catch (e) { 50 | return err 51 | } 52 | return err 53 | } 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /build/cls/WebTerminal/Router.cls: -------------------------------------------------------------------------------- 1 | /// The REST interface: class that routes HTTP requests 2 | Class WebTerminal.Router Extends %CSP.REST [ CompileAfter = StaticContent ] 3 | { 4 | 5 | XData UrlMap 6 | { 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | } 16 | 17 | /// Calls StaticContent.Write method or sends not modified header. Type have to be "css" or "js" 18 | ClassMethod WriteStatic(type As %String, ContentType As %String = "") [ Private ] 19 | { 20 | #define CompileTime ##Expression("""" _ $zd($h, 11) _ ", "_ $zdt($NOW(0), 2,1) _ " GMT""") 21 | set %response.CharSet = "utf-8" 22 | set %response.ContentType = $case(type, 23 | "css": "text/css", 24 | "js": "text/javascript", 25 | "html": "text/html", 26 | : $case(ContentType="", 1:"text/plain", :ContentType) 27 | ) 28 | do %response.SetHeader("Last-Modified", $$$CompileTime) 29 | 30 | if (%request.GetCgiEnv("HTTP_IF_MODIFIED_SINCE")=$$$CompileTime) { 31 | set %response.Status = "304 Not Modified" 32 | } else { 33 | do ##class(StaticContent).Write(type) 34 | } 35 | } 36 | 37 | ClassMethod Auth() As %Status 38 | { 39 | set cookie = $System.Encryption.Base64Encode(%session.Key) 40 | set ^WebTerminal("AuthUser", cookie) = $LB( // authList 41 | $Username, // username 42 | $Horolog, // granting ticket date 43 | $Get(%request.Data("ns", 1), $Get(%request.Data("NS", 1))), 44 | $Get(%request.Data("clean", 1), 0) '= 0 45 | ) 46 | write "{""key"":""" _ cookie _ """}" 47 | return $$$OK 48 | } 49 | 50 | /// Method writes application CSS. 51 | ClassMethod GetCss() As %Status 52 | { 53 | do ..WriteStatic("css") 54 | return $$$OK 55 | } 56 | 57 | /// Method writes application theme. 58 | ClassMethod GetTheme(Theme As %String) As %Status 59 | { 60 | do ..WriteStatic("Theme"_$REPLACE(Theme, ".css", ""),"text/css") 61 | return $$$OK 62 | } 63 | 64 | /// Method writes application JavaScript. 65 | ClassMethod GetJs() As %Status 66 | { 67 | do ..WriteStatic("js") 68 | return $$$OK 69 | } 70 | 71 | /// Method writes application HTML. 72 | ClassMethod Index() As %Status 73 | { 74 | do ..WriteStatic("html") 75 | return $$$OK 76 | } 77 | 78 | } 79 | 80 | -------------------------------------------------------------------------------- /build/cls/WebTerminal/Trace.cls: -------------------------------------------------------------------------------- 1 | Class WebTerminal.Trace Extends Common 2 | { 3 | 4 | /// Property is used to store watching files/globals. 5 | Property Watches As %List; 6 | 7 | /// Watch position in file or global 8 | Property WatchesCaret As %Numeric [ MultiDimensional ]; 9 | 10 | /// Checks for correct watch source and sets watch target to ..Watches 11 | /// Returns status of this operation 12 | Method Trace(name) As %Status 13 | { 14 | set s = $CHAR(0) 15 | set watches = s _ $LISTTOSTRING(..Watches, s) _ s 16 | if ($FIND(watches, s_name_s) '= 0) return 0 // if watch already defined 17 | 18 | if ($EXTRACT(name,1,1) = "^") { // watching global 19 | set g = 0 20 | try { 21 | if (($data(@name)) '= 0) set g = 1 22 | } catch { } 23 | set $ZERROR = "" 24 | if (g = 1) { 25 | set ..Watches = ..Watches _ $LISTBUILD(name) 26 | set ..WatchesCaret(name, 0) = $QUERY(@name@(""), -1) // last 27 | set ..WatchesCaret(name, 1) = "?" 28 | return 1 29 | } 30 | } else { // watch file 31 | if (##class(%File).Exists(name)) { 32 | set ..Watches = ..Watches _ $LISTBUILD(name) 33 | set file = ##class(%File).%New(name) 34 | set ..WatchesCaret(name,0) = file.Size // current watch cursor position 35 | set ..WatchesCaret(name,1) = file.DateModified 36 | return 1 37 | } 38 | } 39 | 40 | return 0 41 | } 42 | 43 | /// Removes watch from watches list 44 | /// Returns success status 45 | Method StopTracing(name) As %Status 46 | { 47 | set s = $CHAR(0) 48 | set watches = s _ $LISTTOSTRING(..Watches,s) _ s 49 | set newWatches = $REPLACE(watches, s_name_s, s) 50 | set ..Watches = $LISTFROMSTRING($EXTRACT(newWatches, 2, *-1), s) 51 | if (watches '= newWatches) { 52 | kill ..WatchesCaret(name) 53 | } 54 | return watches '= newWatches 55 | } 56 | 57 | /// Returns a list current watches 58 | Method ListWatches() As %String 59 | { 60 | set no = 0 61 | set s = "Watching: " _ $CHAR(10) 62 | while $LISTNEXT(..Watches, no, value) { 63 | set s = s_"(pos: "_..WatchesCaret(value,0)_ 64 | "; mod: "_..WatchesCaret(value,1)_") "_value_$CHAR(10) 65 | } 66 | return s 67 | } 68 | 69 | /// Return null string if global hadn't been updated 70 | /// This method watches only for tail of global and detects if global still alive 71 | Method GetTraceGlobalModified(watch) As %List 72 | { 73 | set data = "" 74 | if ($data(@watch)=0) { 75 | do ..StopTracing(watch) 76 | return $lb($C(27)_"[(wrong)m[D]"_$C(27)_"[0m", $C(13, 10)) 77 | } 78 | for { 79 | set query = $QUERY(@..WatchesCaret(watch,0)) 80 | quit:query="" 81 | set ..WatchesCaret(watch,0) = query 82 | set data = data _ $case(data = "", 1: "", :$CHAR(13, 10)) _ @query 83 | } 84 | return $lb($C(27)_"[(special)m[M]"_$C(27)_"[0m", data) 85 | } 86 | 87 | Method GetTraceFileModified(watch) As %String 88 | { 89 | set file=##class(%File).%New(watch) 90 | set size = file.Size 91 | set modDate = file.DateModified 92 | set output = "" 93 | if (size < 0) { // file had been deleted 94 | do ..StopTracing(watch) 95 | return $lb($C(27)_"[(wrong)m[D]"_$C(27)_"[0m", $C(13, 10)) 96 | } 97 | 98 | if (size > ..WatchesCaret(watch, 0)) { 99 | 100 | set stream = ##class(%Stream.FileCharacter).%New() 101 | set sc = stream.LinkToFile(watch) 102 | do stream.MoveTo(..WatchesCaret(watch, 0) + 1) 103 | set read = stream.Read(size - ..WatchesCaret(watch, 0)) 104 | set output = output _ read 105 | set ..WatchesCaret(watch, 0) = size 106 | set ..WatchesCaret(watch, 1) = file.DateModified 107 | return $lb($C(27)_"[(constant)m[A]"_$C(27)_"[0m", output) 108 | 109 | } elseif ((size < ..WatchesCaret(watch, 0)) || (file.DateModified '= ..WatchesCaret(watch, 1))) { 110 | 111 | set output = output _ "Size change: " _ (size - ..WatchesCaret(watch, 0)) 112 | set ..WatchesCaret(watch, 0) = size 113 | set ..WatchesCaret(watch, 1) = file.DateModified 114 | return $lb($C(27)_"[(special)m[M]"_$C(27)_"[0m", output) 115 | 116 | } // else file not changed 117 | 118 | return $lb("", "") 119 | } 120 | 121 | Method CheckTracing() As %String 122 | { 123 | set no = 0 124 | set data = "" 125 | set overall = "" 126 | set watchList = ..Watches // do not remove or simplify: ..Watches can be modified 127 | while $LISTNEXT(watchList, no, value) { 128 | set global = $EXTRACT(value, 1, 1) = "^" 129 | if global { 130 | set data = ..GetTraceGlobalModified(value) 131 | } else { 132 | set data = ..GetTraceFileModified(value) 133 | } 134 | if ($LISTGET(data, 2) '= "") { 135 | set overall = $LISTGET(data, 1) _ " " _ $C(27) _ "[2m" _ $ZDATETIME($NOW(),1,1) 136 | _ $C(27) _ "[0m " _ $C(27) _ "[(" _ $case(global, 1: "global", :"string") _ ")m" 137 | _ value _ $C(27) _ "[0m" _ $CHAR(13, 10) _ $LISTGET(data, 2) _ $CHAR(13, 10) 138 | } 139 | set data = "" 140 | } 141 | return overall 142 | } 143 | 144 | } 145 | 146 | -------------------------------------------------------------------------------- /build/cls/WebTerminal/Updater.cls: -------------------------------------------------------------------------------- 1 | /// Web Terminal version 4.9.5 update module class. 2 | /// This class represents update mechanism for WebTerminal. Internet connection is required to 3 | /// update WebTerminal. 4 | Class WebTerminal.Updater 5 | { 6 | 7 | /// SSL configuration name used for HTTPS requests. 8 | Parameter SSLConfigName = "WebTerminalSSL"; 9 | 10 | ClassMethod GetSSLConfigurationName() As %String 11 | { 12 | new $namespace 13 | zn "%SYS" 14 | if ('##class(Security.SSLConfigs).Exists(..#SSLConfigName)) { 15 | set st = ##class(Security.SSLConfigs).Create(..#SSLConfigName) 16 | return:(st '= 1) "UnableToCreateSSLConfig:"_$System.Status.GetErrorText(st) 17 | } 18 | return ..#SSLConfigName 19 | } 20 | 21 | ClassMethod WriteAndDelete(client As WebTerminal.Engine, file As %String) As %Status 22 | { 23 | if ##class(%File).Exists(file) { 24 | set stream = ##class(%Stream.FileCharacter).%New() 25 | set sc = stream.LinkToFile(file) 26 | while 'stream.AtEnd { 27 | do client.Send("o", stream.Read()_$CHAR(13, 10)) 28 | } 29 | do client.Send("oLocalized", "%sUpdCleanLog("_file_")"_$CHAR(13, 10)) 30 | do ##class(%File).Delete(file) 31 | } else { 32 | do client.Send("oLocalized", "%sUpdNoFile") 33 | } 34 | return $$$OK 35 | } 36 | 37 | ClassMethod Stop(client As WebTerminal.Engine, status As %Status) As %Status 38 | { 39 | if ($$$ISERR(status)) { 40 | do client.Send("oLocalized", "%sUpdErr("_$System.Status.GetErrorText(status)_")"_$C(13, 10)) 41 | } 42 | return status 43 | } 44 | 45 | ClassMethod Update(client As WebTerminal.Engine, URL As %String) As %Status 46 | { 47 | do client.Send("oLocalized", "%sUpdSt"_$CHAR(13, 10)) 48 | set request = ##class(%Net.HttpRequest).%New() 49 | set request.Server = $PIECE(URL, "/", 3) 50 | set request.Location = $PIECE(URL, "/", 4, *) 51 | set request.Https = 1 52 | set request.SSLConfiguration = ..GetSSLConfigurationName() 53 | do client.Send("oLocalized", "%sUpdRURL(https://"_request.Server_"/"_request.Location_")"_$CHAR(13, 10)) 54 | set status = request.Get() 55 | do client.Send("oLocalized", "%sUpdGetOK"_$CHAR(13, 10)) 56 | return:(status '= $$$OK) status 57 | 58 | if (request.HttpResponse.StatusCode '= 200) { 59 | do client.Send("oLocalized", "%sUpdSCode("_request.HttpResponse.StatusCode_")"_$CHAR(13, 10)) 60 | return $$$ERROR($$$GeneralError, "HTTP "_request.HttpResponse.StatusCode) 61 | } 62 | 63 | do client.Send("oLocalized", "%sUpdWTF"_$CHAR(13, 10)) 64 | set tempFile = ##class(%File).TempFilename("xml") 65 | set file = ##class(%File).%New(tempFile) 66 | do file.Open("NW") 67 | set data = request.HttpResponse.Data 68 | if (($IsObject(data)) && (data.%IsA("%Stream.Object"))) { 69 | while 'data.AtEnd { 70 | set chunk = data.Read(data.Size) 71 | do file.Write(chunk) 72 | } 73 | } else { 74 | do file.Write(data) 75 | } 76 | do file.Close() 77 | 78 | set backupFile = ##class(%File).TempFilename("xml") 79 | do client.Send("oLocalized", "%sUpdBack("_backupFile_")"_$CHAR(13, 10)) 80 | 81 | set logFile = ##class(%File).TempFilename("txt") 82 | set io = $IO 83 | 84 | open logFile:("NW") 85 | use logFile 86 | set exportStatus = $System.OBJ.Export("WebTerminal.*.CLS", backupFile) 87 | close logFile 88 | use io 89 | 90 | do ..WriteAndDelete(client, logFile) 91 | if ($$$ISERR(exportStatus)) { 92 | do ##class(WebTerminal.Analytics).ReportInstallStatus(exportStatus, "Update") 93 | return ..Stop(client, exportStatus) 94 | } 95 | do client.Send("oLocalized", "%sUpdRemLoad"_$CHAR(13, 10)) 96 | 97 | open logFile:("NW") 98 | use logFile 99 | write $C(27)_"[2m" 100 | do $System.OBJ.DeletePackage("WebTerminal") 101 | write $C(27)_"[0m", ! 102 | set loadStatus = $SYSTEM.OBJ.Load(tempFile, "c") 103 | 104 | // At this moment WebTerminal's code can be broken, totally changed or deleted. Do not call 105 | // WebTerminal's methods until terminal is restored / fully updated 106 | 107 | if '$$$ISOK(loadStatus) { // roll back 108 | write !, $C(27)_"[(wrong)m==FAILED=="_$C(27)_"[0m", !, 109 | $System.Status.GetErrorText(loadStatus), !, !, 110 | $C(27)_"[(special)m==RESTORING=="_$C(27)_"[0m", ! 111 | do $SYSTEM.OBJ.Load(backupFile, "c") 112 | } 113 | 114 | // end 115 | 116 | close logFile 117 | use io 118 | do ..WriteAndDelete(client, logFile) 119 | 120 | do client.Send("oLocalized", "%sUpdClean("_tempFile_")"_$CHAR(13, 10)) 121 | do ##class(%File).Delete(tempFile) 122 | do client.Send("oLocalized", "%sUpdClean("_backupFile_")"_$CHAR(13, 10, 13, 10)) 123 | do ##class(%File).Delete(backupFile) 124 | if '$$$ISOK(loadStatus) { 125 | do ..Stop(client, loadStatus) 126 | do client.Send("oLocalized", $CHAR(13, 10)_"%sUpdRes"_$CHAR(13, 10)) 127 | } 128 | do client.Send("oLocalized", "%sUpdDone"_$CHAR(13, 10)) 129 | 130 | do ##class(WebTerminal.Analytics).ReportInstallStatus(loadStatus, "Update") 131 | 132 | return $$$OK 133 | } 134 | 135 | } 136 | 137 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | Contributing Guidelines 2 | ======================= 3 | 4 | We are happy to see anyone who is interested in improving the WebTerminal project! 5 | All the bugs you find, issues you discover and things you report make WebTerminal project grow! 6 | 7 | Feel free to [submit](https://github.com/intersystems-ru/webterminal/issues) any questions, issues, feature requests or bug reports to this project. 8 | 9 | If You Want To Implement Something... 10 | ===================================== 11 | 12 | Please, read [Contributing Guidelines](http://intersystems-ru.github.io/webterminal/#docs.5) at the project's page. This will be useful if you want to implement some features or fix something by yourself. This project is always open for pull requests! 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | iris: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | restart: always 8 | command: --check-caps false 9 | ports: 10 | - 51663:1972 11 | - 52663:52773 12 | - 53773 13 | volumes: 14 | - ./:/irisdev/app 15 | -------------------------------------------------------------------------------- /docs/css/content.css: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes rainbow { 2 | 0% { 3 | color: rgb(0, 255, 0); 4 | } 5 | 6 | 33% { 7 | color: rgb(0, 0, 255); 8 | } 9 | 10 | 66% { 11 | color: rgb(255, 0, 0); 12 | } 13 | 14 | 100% { 15 | color: rgb(0, 255, 0); 16 | } 17 | } 18 | 19 | @-moz-keyframes rainbow { 20 | 0% { 21 | color: rgb(0, 255, 0); 22 | } 23 | 24 | 33% { 25 | color: rgb(0, 0, 255); 26 | } 27 | 28 | 66% { 29 | color: rgb(255, 0, 0); 30 | } 31 | 32 | 100% { 33 | color: rgb(0, 255, 0); 34 | } 35 | } 36 | 37 | @-o-keyframes rainbow { 38 | 0% { 39 | color: rgb(0, 255, 0); 40 | } 41 | 42 | 33% { 43 | color: rgb(0, 0, 255); 44 | } 45 | 46 | 66% { 47 | color: rgb(255, 0, 0); 48 | } 49 | 50 | 100% { 51 | color: rgb(0, 255, 0); 52 | } 53 | } 54 | 55 | @keyframes rainbow { 56 | 0% { 57 | color: rgb(0, 255, 0); 58 | } 59 | 60 | 33% { 61 | color: rgb(0, 0, 255); 62 | } 63 | 64 | 66% { 65 | color: rgb(255, 0, 0); 66 | } 67 | 68 | 100% { 69 | color: rgb(0, 255, 0); 70 | } 71 | } 72 | 73 | .rainbowText { 74 | text-shadow: 1px 1px 1px #ffffff; 75 | -webkit-animation: rainbow 4s infinite linear; 76 | -moz-animation: rainbow 4s infinite linear; 77 | -o-animation: rainbow 4s infinite linear; 78 | animation: rainbow 4s infinite linear; 79 | } 80 | 81 | .imgFloatLeft { 82 | float: left; 83 | margin: 5px; 84 | } 85 | 86 | .imgFloatRight { 87 | float: right; 88 | margin: 5px; 89 | } 90 | 91 | .center { 92 | text-align: center; 93 | } 94 | 95 | .screenShoot { 96 | max-width: 60%; 97 | box-shadow: 4px 4px 6px black; 98 | margin: 15px; 99 | } 100 | 101 | .clearHL { 102 | clear: both; 103 | } 104 | 105 | .warn { 106 | color: #ff6500 107 | } 108 | 109 | .newsBlock { 110 | overflow: hidden; 111 | position: relative; 112 | background: rgba(255, 255, 255, 0.2); 113 | border-radius: 5px; 114 | padding: .3em; 115 | } 116 | 117 | .newsBlock h1 { 118 | margin: 4px; 119 | font-size: 20px; 120 | text-decoration: underline; 121 | } 122 | 123 | .newsBlock .date { 124 | float: right; 125 | font-style: italic; 126 | } 127 | 128 | .newsBlock .content { 129 | clear: both; 130 | text-indent: 2.5em; 131 | } 132 | 133 | .newsBlock ~ .newsBlock { 134 | margin-top: 10px; 135 | } 136 | 137 | #showcase > p:after { 138 | content: ""; 139 | display: block; 140 | clear: both; 141 | } 142 | 143 | /* Terminal Colors */ 144 | 145 | 146 | .g.m1 { /* bright */ 147 | font-weight: 900; 148 | } 149 | 150 | .g.m2 { /* dim */ 151 | opacity: 0.7; 152 | } 153 | 154 | .g.m3 { 155 | font-style: italic; 156 | } 157 | 158 | .g.m4 { 159 | text-decoration: underline; 160 | } 161 | 162 | .g.m5 { 163 | @include animation(blink infinite 1s); 164 | } 165 | 166 | .g.m7 { 167 | @include filter(invert(100%)); 168 | } 169 | 170 | .g.m8 { 171 | opacity: 0; 172 | } 173 | 174 | .g.hint { 175 | opacity: .5; 176 | } 177 | 178 | .g.m30 { 179 | color: #000000; 180 | } 181 | 182 | .g.m31 { 183 | color: #ff0000; 184 | } 185 | 186 | .g.m32 { 187 | color: #008000; 188 | } 189 | 190 | .g.m33 { 191 | color: yellow; 192 | } 193 | 194 | .g.m34 { 195 | color: #0000ff; 196 | } 197 | 198 | .g.m35 { 199 | color: magenta; 200 | } 201 | 202 | .g.m36 { 203 | color: cyan; 204 | } 205 | 206 | .g.m37 { 207 | color: white; 208 | } 209 | 210 | .g.m40 { 211 | background-color: #000000; 212 | } 213 | 214 | .g.m41 { 215 | background-color: #ff0000; 216 | } 217 | 218 | .g.m42 { 219 | background-color: #008000; 220 | } 221 | 222 | .g.m43 { 223 | background-color: yellow; 224 | } 225 | 226 | .g.m44 { 227 | background-color: #0000ff; 228 | } 229 | 230 | .g.m45 { 231 | background-color: magenta; 232 | } 233 | 234 | .g.m46 { 235 | background-color: cyan; 236 | } 237 | 238 | .g.m47 { 239 | background-color: white; 240 | } 241 | 242 | .g.keyword { 243 | color: #4898ff; 244 | } 245 | 246 | .g.string { 247 | color: #00c700; 248 | } 249 | 250 | .g.constant { 251 | color: cyan; 252 | } 253 | 254 | .g.special { 255 | color: yellow; 256 | } 257 | 258 | .g.variable { 259 | color: lightsalmon; 260 | } 261 | 262 | .g.wrong { 263 | color: red; 264 | } 265 | 266 | .g.selected { 267 | color: white; 268 | background-color: royalblue; 269 | } 270 | 271 | .g.argument { 272 | color: magenta; 273 | } 274 | 275 | .segments { 276 | position: relative; 277 | overflow: hidden; 278 | } 279 | 280 | @media (min-width: 1200px) { 281 | .segments { 282 | column-count: 2; 283 | column-gap: 0; 284 | } 285 | .segments > .segment { 286 | break-inside: avoid; 287 | padding: 5px; 288 | } 289 | } -------------------------------------------------------------------------------- /docs/css/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: url("../img/back.png") black; 3 | color: white; 4 | font-family: 'PT Sans Narrow', sans-serif; 5 | font-size: 18px !important; 6 | text-shadow: 1px 1px 1px #000000; 7 | min-width: 539px; 8 | } 9 | 10 | body { 11 | position: relative; 12 | margin: 0; 13 | padding: 0 80px 180px 80px; 14 | } 15 | 16 | img { 17 | border: none; 18 | } 19 | 20 | p { 21 | text-indent: 2.5em; 22 | text-align: justify; 23 | } 24 | 25 | a { 26 | text-decoration: none; 27 | color: #fffc9e; 28 | transition: all 0.3s ease; 29 | } 30 | 31 | sup { 32 | position: relative; 33 | } 34 | 35 | a:hover { 36 | text-shadow: 0 0 5px #fff000; 37 | } 38 | 39 | .pageHeader { 40 | font-size: 50px; 41 | position: absolute; 42 | left: -60px; 43 | top: -70px; 44 | white-space: nowrap; 45 | text-shadow: 1px 1px 5px #fff063; 46 | } 47 | 48 | .pageBody { 49 | position: relative; 50 | top: 80px; 51 | overflow: visible; 52 | min-height: 520px; 53 | background: rgba(255,255,255,0.2); 54 | border-radius: 0 20px/100px; 55 | } 56 | 57 | .pageLayout, .pageLayout:target ~ #main, #main.inactive { 58 | position: relative; 59 | padding: 0; 60 | height: 0; 61 | overflow: hidden; 62 | opacity: 0; 63 | transition: all 0.3s ease; 64 | } 65 | 66 | #main, .pageLayout:target, .pageLayout.active { 67 | display: block; 68 | opacity: 1; 69 | padding: 20px 20px 20px 20px; 70 | height: auto; 71 | } 72 | 73 | .pageFooter { 74 | position: absolute; 75 | background: rgba(255,255,255,0.2); 76 | border-radius: 0 0 15px 15px; 77 | right: 0; 78 | height: 30px; 79 | bottom: -30px; 80 | padding: 0 6px 0 6px; 81 | color: #acacac; 82 | } 83 | 84 | .siteNavigator { 85 | position: absolute; 86 | list-style: none; 87 | padding: 0; 88 | margin: 0; 89 | display: block; 90 | left: -64px; 91 | width: 64px; 92 | } 93 | 94 | .siteNavigator > li { 95 | position: relative; 96 | width: 64px; 97 | height: 64px; 98 | background: rgba(255,255,255,0.2); 99 | border-radius: 15px 0 0 15px; 100 | margin-bottom: 6px; 101 | cursor: pointer; 102 | } 103 | 104 | .siteNavigator > li > a > img { 105 | position: relative; 106 | left: 10%; 107 | top: 10%; 108 | width: 80%; 109 | height: 80%; 110 | transition: all 0.1s ease; 111 | } 112 | 113 | .siteNavigator > li > a > img:hover { 114 | left: 0; 115 | top: 0; 116 | width: 100%; 117 | height: 100%; 118 | } 119 | 120 | #forkLink { 121 | position: absolute; 122 | right: 0; 123 | top: 0; 124 | display: block; 125 | z-index: 1; 126 | } 127 | 128 | table { 129 | border-collapse: collapse; 130 | box-shadow: 0 0 1px white; 131 | border-radius: 5px; 132 | } 133 | 134 | table tr:nth-child(2n) { 135 | background-color: rgba(255,255,255,0.2); 136 | } 137 | 138 | table th { 139 | background-color: rgba(255, 252, 158, 0.5); 140 | } 141 | 142 | table th:first-child { 143 | border-radius: 5px 0 0 0; 144 | } 145 | 146 | table th:last-child { 147 | border-radius: 0 5px 0 0; 148 | } 149 | 150 | table tbody tr:last-child td:first-child { 151 | border-radius: 0 0 0 5px; 152 | } 153 | 154 | table tbody tr:last-child td:last-child { 155 | border-radius: 0 0 5px 0; 156 | } 157 | 158 | table td { 159 | padding: 2px; 160 | } 161 | 162 | code { 163 | display: block; 164 | background-color: rgba(255,255,255,0.1); 165 | text-indent: 0; 166 | padding: .5em; 167 | margin: .3em 0; 168 | border-radius: 5px; 169 | overflow: auto; 170 | font-size: 12px; 171 | } 172 | 173 | code.inline { 174 | display: inline-block; 175 | margin: 0; 176 | font-size: 95%; 177 | padding: 1px; 178 | vertical-align: bottom; 179 | } 180 | 181 | code.pre { 182 | white-space: pre; 183 | font-size: 12px; 184 | } 185 | 186 | OL { 187 | counter-reset: list1; 188 | padding-left: 2.5em; 189 | } 190 | UL { 191 | padding-left: 2.5em; 192 | } 193 | OL LI { 194 | list-style-type: none; 195 | } 196 | OL LI:before { 197 | counter-increment: list1; 198 | content: counter(list1) ". "; 199 | } 200 | OL OL { counter-reset: list2; } 201 | OL OL LI:before { 202 | counter-increment: list2; 203 | content: counter(list1) "." counter(list2) ". "; 204 | } 205 | OL OL OL { counter-reset: list3; } 206 | OL OL OL LI:before { 207 | counter-increment: list3; 208 | content: counter(list1) "." counter(list2) "." counter(list3) ". "; 209 | } 210 | 211 | h1:target, h2:target, h3:target { 212 | color: #fffc9e; 213 | animation: highlight 2s ease-out; 214 | -moz-animation: highlight 2s ease-out; 215 | -o-animation: highlight 2s ease-out; 216 | -webkit-animation: highlight 2s ease-out; 217 | text-decoration: underline; 218 | } 219 | 220 | @keyframes highlight { 221 | 0% { color: inherit } 222 | 50% { color: orange; padding-left: 10px; } 223 | 100% { color: #fffc9e } 224 | } 225 | 226 | @-moz-keyframes highlight { 227 | 0% { color: inherit } 228 | 50% { color: orange; padding-left: 10px; } 229 | 100% { color: #fffc9e } 230 | } 231 | 232 | @-ms-keyframes highlight { 233 | 0% { color: inherit } 234 | 50% { color: orange; padding-left: 10px; } 235 | 100% { color: #fffc9e } 236 | } 237 | 238 | @-o-keyframes highlight { 239 | 0% { color: inherit } 240 | 50% { color: orange; padding-left: 10px; } 241 | 100% { color: #fffc9e } 242 | } 243 | 244 | @-webkit-keyframes highlight { 245 | 0% { color: inherit } 246 | 50% { color: orange; padding-left: 10px; } 247 | 100% { color: #fffc9e } 248 | } -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/favicon.ico -------------------------------------------------------------------------------- /docs/img/WebTerminal Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/WebTerminal Preview.png -------------------------------------------------------------------------------- /docs/img/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/back.png -------------------------------------------------------------------------------- /docs/img/fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/fork.png -------------------------------------------------------------------------------- /docs/img/icons/docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/icons/docs.png -------------------------------------------------------------------------------- /docs/img/icons/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/icons/download.png -------------------------------------------------------------------------------- /docs/img/icons/feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/icons/feedback.png -------------------------------------------------------------------------------- /docs/img/icons/images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/icons/images.png -------------------------------------------------------------------------------- /docs/img/icons/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/icons/save.png -------------------------------------------------------------------------------- /docs/img/icons/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/icons/terminal.png -------------------------------------------------------------------------------- /docs/img/logos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/logos.png -------------------------------------------------------------------------------- /docs/img/screenshoots/ac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/screenshoots/ac.png -------------------------------------------------------------------------------- /docs/img/screenshoots/features-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/screenshoots/features-config.png -------------------------------------------------------------------------------- /docs/img/screenshoots/features-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/screenshoots/features-help.png -------------------------------------------------------------------------------- /docs/img/screenshoots/features-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/screenshoots/features-mobile.png -------------------------------------------------------------------------------- /docs/img/screenshoots/install-steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/screenshoots/install-steps.png -------------------------------------------------------------------------------- /docs/img/screenshoots/scr4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/screenshoots/scr4.png -------------------------------------------------------------------------------- /docs/img/screenshoots/showcase-slq-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/webterminal/91b54afa0ef4c80d5ac2d114df55b07de72e3864/docs/img/screenshoots/showcase-slq-mode.png -------------------------------------------------------------------------------- /docs/latestNightVersion: -------------------------------------------------------------------------------- 1 | 9999#WebTerminal-v4.0.0-beta.1 -------------------------------------------------------------------------------- /docs/latestVersion: -------------------------------------------------------------------------------- 1 | 26#WebTerminal-v4.0.0-beta.1 -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # Caché Web Terminal Project Website's Sources 2 | 3 | This (`/docs`) directory is served by GitHub at https://intersystems-community.github.io/webterminal. 4 | 5 | You are welcome to fix all found mistakes or typos on the project's website. 6 | Do do this, fork this repository, edit corresponding files and make a pull request. 7 | Find more about contributing to WebTerminal project [here](https://intersystems-community.github.io/webterminal/#docs.5). 8 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from "gulp"; 2 | import pkg from "./package.json"; 3 | import cssNano from "gulp-cssnano"; 4 | import uglify from "gulp-uglify"; 5 | import replace from "gulp-replace"; 6 | import rimraf from "gulp-rimraf"; 7 | import scss from "gulp-sass"; 8 | import rename from "gulp-rename"; 9 | import preprocess from "gulp-preprocess"; 10 | import browserify from "browserify"; 11 | import "babelify"; 12 | import sourceStream from "vinyl-source-stream"; 13 | import buffer from "vinyl-buffer"; 14 | import fs from "fs"; 15 | import preprocessify from "preprocessify"; 16 | //import sourcemaps from "gulp-sourcemaps"; 17 | import { getAutomaton } from "./src/client/js/parser/_build"; 18 | 19 | let INSTALLER_CLASS_NAME = `${ pkg["packageName"] }.Installer`; 20 | 21 | let dir = __dirname, 22 | dest = `${dir}/build`, 23 | source = `${dir}/src`, 24 | context = { 25 | includeBase: dest, 26 | context: { 27 | package: pkg, 28 | compileAfter: "", // is set during "pre-cls" task. 29 | themes: "", // is set after css move task 30 | autocompleteAutomaton: [], 31 | ruleMappings: {} 32 | } 33 | }, 34 | themes = []; // reassigned 35 | 36 | const sass = require('gulp-sass')(require('sass')); 37 | 38 | function themesReady () { // triggered when build is done 39 | themes = fs.readdirSync(`${ dest }/client/css/themes`); 40 | context.context.themes = themes.map(function (n) { 41 | return ', "' + n.replace(/\..*$/, "") + '": "css/themes/' + n + '"'; 42 | }).join(""); 43 | } 44 | 45 | gulp.task("prepare", function (cb) { 46 | let aut = []; 47 | console.log(`Compiling autocomplete and highlight rules...`); 48 | try { 49 | aut = getAutomaton(); 50 | context.context.autocompleteAutomaton = JSON.stringify(aut.automaton); 51 | context.context.ruleMappings = JSON.stringify(aut.ruleMappings); 52 | } catch (e) { 53 | console.error.apply(console, e); 54 | cb(e); 55 | } 56 | console.log(`Automaton ready and has ${ aut.automaton.length } states with ${ 57 | aut.automaton.reduce((a, b) => 58 | (typeof a === "number" ? a : a.length) + (typeof b === "number" ? b : b.length)) 59 | } rules.`); 60 | cb(); 61 | }); 62 | 63 | gulp.task("clean", gulp.series("prepare", function () { 64 | return gulp.src(dest, { read: false, allowEmpty: true }) 65 | .pipe(rimraf()); 66 | })); 67 | 68 | gulp.task("html", gulp.series(function () { 69 | return gulp.src(`${ source }/client/index.html`) 70 | .pipe(preprocess(context)) 71 | .pipe(gulp.dest(`${ dest }/client`)); 72 | })); 73 | 74 | gulp.task("scss", gulp.series(() => { 75 | return gulp.src([`${source}/client/scss/index.scss`]) 76 | .pipe(preprocess(context)) 77 | .pipe(sass()) 78 | .pipe(cssNano({ 79 | zindex: false 80 | })) 81 | .pipe(gulp.dest(`${dest}/client/css`)); 82 | })); 83 | 84 | gulp.task("copy-css-themes", gulp.series(function () { 85 | return gulp.src(`${ source }/client/scss/themes/*.*`) 86 | .pipe(preprocess(context)) 87 | .pipe(sass()) 88 | .pipe(cssNano()) 89 | .pipe(gulp.dest(`${ dest }/client/css/themes/`)); 90 | })); 91 | 92 | // Need css themes directory copied to collect themes names. 93 | gulp.task("css", gulp.series("scss", "copy-css-themes", function (cb) { 94 | themesReady(); 95 | cb(); 96 | })); 97 | 98 | gulp.task("js", gulp.series("css", function () { 99 | let bundler = browserify({ 100 | entries: `${source}/client/js/index.js`, 101 | debug: true 102 | }).transform(preprocessify, { 103 | includeExtensions: ['.js'], 104 | context: context.context 105 | }); 106 | bundler.transform("babelify", { presets: ["es2015"] }); 107 | return bundler.bundle() 108 | .on("error", function (err) { console.error("An error occurred during bundling:", err); }) 109 | .pipe(sourceStream("index.js")) 110 | .pipe(buffer()) 111 | //.pipe(sourcemaps.init({ loadMaps: true })) 112 | .pipe(uglify({ 113 | output: { 114 | ascii_only: true, 115 | width: 25000, 116 | max_line_len: 15000, 117 | comments: "some" 118 | }, 119 | })) 120 | .pipe(replace(/\x0b|\x1b/g, e => `\\x${ e === "\x0b" ? 0 : 1 }b`)) 121 | .pipe(replace(/[\x00-\x08]/g, e => `\\x0${ e.charCodeAt(0) }`)) 122 | //.pipe(sourcemaps.write()) 123 | .pipe(gulp.dest(`${ dest }/client/js`)); 124 | })); 125 | 126 | gulp.task("readme", gulp.series(function () { 127 | return gulp.src(`${ dir }/readme.md`) 128 | .pipe(gulp.dest(`${ dest }`)); 129 | })); 130 | 131 | gulp.task("cls", gulp.series("js", "js", "html", "css", "readme", () => { 132 | return gulp.src([`${ source }/cls/**/*.cls`]) 133 | .pipe(preprocess(context)) 134 | .pipe(gulp.dest(`${dest}/cls`)); 135 | })); 136 | 137 | gulp.task("default", gulp.series("clean", "cls")); -------------------------------------------------------------------------------- /import.bat: -------------------------------------------------------------------------------- 1 | :: This batch script makes the Caché application deployment much faster by building, importing and 2 | :: exporting the XML the project. Replace the path below to your Caché installation and 3 | :: build & import application to Caché using only one command. 4 | 5 | :: Latest NodeJS & Caché 2016.2+ IS REQUIRED TO PROCEED 6 | @echo off 7 | 8 | :: CHANGE THIS PATH TO YOUR CACHÉ INSTALLATION PATH ON WINDOWS (folder that contains bin, CSP, mgr and other folders) 9 | set CACHE_DIR=C:\Program Files\Ensemble-2017 10 | :: NAMESPACE TO IMPORT PACKAGE TO 11 | set NAMESPACE=USER 12 | :: Other variables 13 | set BUILD_DIR=build\cls 14 | :: Export 15 | set XML_EXPORT_DIR=build 16 | set PACKAGE_NAME=WebTerminal 17 | 18 | :: Build and import application to Caché 19 | echo Building the project... 20 | npm run build && ^ 21 | echo s st = $system.Status.GetErrorText($system.OBJ.ImportDir("%~dp0%BUILD_DIR%",,"ck",,1)) w "IMPORT STATUS: "_$case(st="",1:"OK",:st) halt | "%CACHE_DIR%\bin\cache.exe" -s "%CACHE_DIR%\mgr" -U %NAMESPACE% && ^ 22 | echo s st = $system.Status.GetErrorText($system.OBJ.ExportPackage("%PACKAGE_NAME%", "%~dp0%XML_EXPORT_DIR%\%PACKAGE_NAME%-v"_##class(%PACKAGE_NAME%.Installer).#VERSION_".xml")) w $c(13,10)_"EXPORT STATUS: "_$case(st="",1:"OK",:st) halt | "%CACHE_DIR%\bin\cache.exe" -s "%CACHE_DIR%\mgr" -U %NAMESPACE% -------------------------------------------------------------------------------- /iris.script: -------------------------------------------------------------------------------- 1 | zn "%SYS" 2 | Do ##class(Security.Users).UnExpireUserPasswords("*") 3 | 4 | zn "USER" 5 | zpm "load /opt/irisapp/ -v":1:1 6 | halt 7 | -------------------------------------------------------------------------------- /issue_template.md: -------------------------------------------------------------------------------- 1 | Thank you for reaching WebTerminal project! Just a few guidelines for you: 2 | 3 | + Feel free to submit questions and feature requests! 4 | + If you are going to submit bug report, please include technical details: Caché version and a way 5 | how to reproduce the problem. Please, submit some demonstration code (when appropriate). 6 | + By using search, check if there were no such issues before. 7 | + Replace this text with your issue! Thanks! 8 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-present Nikita Savchenko (https://nikita.tk) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | webterminal 6 | 4.9.6 7 | true 8 | Web Terminal 9 | module 10 | build 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-terminal", 3 | "title": "Web Terminal", 4 | "packageName": "WebTerminal", 5 | "printableName": "Web Terminal", 6 | "description": "Web-based terminal emulator for InterSystems products.", 7 | "author": { 8 | "name": "Nikita Savchenko", 9 | "url": "https://nikita.tk" 10 | }, 11 | "version": "4.9.5", 12 | "gaID": "UA-83005064-2", 13 | "releaseNumber": 26, 14 | "contributors": [ 15 | { 16 | "name": "Nikita Savchenko", 17 | "email": "me@nikita.tk" 18 | }, 19 | { 20 | "name": "John Murray", 21 | "email": "johnm@georgejames.com" 22 | } 23 | ], 24 | "scripts": { 25 | "build": "gulp" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/intersystems-community/webterminal.git" 30 | }, 31 | "devDependencies": { 32 | "babel-core": "^6.23.1", 33 | "babel-polyfill": "^6.23.0", 34 | "babel-preset-es2015": "^6.22.0", 35 | "babelify": "^7.3.0", 36 | "browserify": "^14.1.0", 37 | "gulp": "^4.0.2", 38 | "gulp-cssnano": "^2.1.3", 39 | "gulp-minify-css": "^1.2.4", 40 | "gulp-preprocess": "^4.0.2", 41 | "gulp-rename": "^2.0.0", 42 | "gulp-replace": "^0.5.4", 43 | "gulp-rimraf": "^1.0.0", 44 | "gulp-sass": "^5.1.0", 45 | "gulp-uglify": "^3.0.2", 46 | "preprocessify": "^1.0.1", 47 | "sass": "^1.53.0", 48 | "vinyl-buffer": "^1.0.1", 49 | "vinyl-source-stream": "^2.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Web Terminal 2 | 3 | [![Gitter](https://img.shields.io/badge/chat-on%20telegram-blue.svg)](https://t.me/joinchat/FoZ4M0jbeW8PVp2l5tqrgg) 4 | 5 | Web-based terminal for InterSystems products. Access your database from everywhere! 6 | 7 | + Visit the [project's page](http://intersystems-community.github.io/webterminal) for more details. 8 | + **Download** the latest version from [here](http://intersystems-community.github.io/webterminal/#downloads). 9 | + Read more and discuss WebTerminal on [InterSystems Developer Community](https://community.intersystems.com/post/cach%C3%A9-webterminal-v4-release). 10 | + Read [complete documentation](http://intersystems-community.github.io/webterminal/#docs) about WebTerminal. 11 | 12 | ### Preview 13 | 14 | Syntax highlighting & intelligent autocomplete! 15 | 16 | ![2016-09-18_212035](https://cloud.githubusercontent.com/assets/4989256/18618027/33a4b544-7de6-11e6-9bf5-a535a2dc4bca.png) 17 | 18 | Embedded SQL mode! 19 | 20 | ![2016-09-18_212244](https://cloud.githubusercontent.com/assets/4989256/18618029/33a7183e-7de6-11e6-9a98-cceacca7b078.png) 21 | 22 | Even more features! 23 | 24 | ![2016-09-18_212325](https://cloud.githubusercontent.com/assets/4989256/18618028/33a4c246-7de6-11e6-9ee9-4970223b0b31.png) 25 | 26 | ### Key Features 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
Native browser applicationAllows to access Caché terminal both from desktop and mobile devices.
AutocompletionType faster. Autocomplete is available for class names, variable and global names, methods, properties, etc.
TracingMonitor any changes in globals or files.
SQL modeA convenient way to execute SQL queries.
Syntax highlightingIntelligently highlighted input both for ObjectScript and SQL.
FavoritesSave commands you execute frequently.
SecurityAll you need is to protect /terminal/ web application, and all sessions are guaranteed to be secure.
Self-updatingWebTerminal of version 4 and higher prompts to update automatically when new version is available, so you will never miss the important update.
Explore!Enjoy using WebTerminal!
65 | 66 | Installation 67 | ------------ 68 | 69 | Download the latest version from the project page and import downloaded XML file into any namespace. Compile imported items and the WebTerminal is ready! 70 | 71 | Usage 72 | ----- 73 | 74 | After installation, you will be able to access application at `http://[host]:[port]/terminal/` (slash at the end is required). 75 | Type `/help` there to get more information. 76 | 77 | Integration and WebTerminal's API 78 | --------------------------------- 79 | 80 | To embed WebTerminal to any other web application, you can use ` 84 | ``` 85 | 86 | Note that terminal URL may include optional GET parameters, which are the next: 87 | 88 | + `ns=USER` Namespace to open terminal in. If the logged user has no access to this namespace, 89 | the error message will appear and no namespace changes will occur. 90 | + `clean` Start the WebTerminal without any additional information printed. It is not recommended to 91 | use this option if you are using terminal as a stand-alone tool (for everyday use), as you can miss 92 | important updates. 93 | 94 | To use WebTerminal's API, you need to get WebTerminal instance first. Use iframe's 95 | `onTerminalInit` function to get it. 96 | 97 | ```js 98 | document.querySelector("#terminal").contentWindow.onTerminalInit(function (terminal) { 99 | // now work with terminal object here! 100 | }); 101 | ``` 102 | 103 | This function is triggered after WebTerminal establish an authorized connection. 104 | The next table demonstrates available API. Left column are `terminal` object properties. 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 123 | 124 | 125 | 126 | 139 | 140 | 141 | 142 | 148 | 149 | 150 | 151 | 156 | 157 | 158 | 159 | 166 | 167 |
FunctionDescription
execute(command, [options], [callback]) 114 | Executes the ObjectScript command right as if it is entered 115 | to the terminal. However, options provide an 116 | additional flags setup.
117 | options.echo (false by default) - prints the 118 | command on the screen.
119 | options.prompt (false by default) - prompts 120 | the user after execution (prints "NAMESPACE > " as well). If callback is passed, 121 | the output buffer will come as a first argument of the callback function. 122 |
onOutput([options], callback) 127 | By default, callback(strings) will be called before the user is 128 | prompted for input, and strings array will always contain an array of 129 | chunks of all the text printed between the prompts. For example, if user writes 130 | write 123 and presses "Enter", the strings will contain 131 | this array: ["\r\n", "123", "\r\n"]. However, when user enters 132 | write 1, 2, 3, strings will result with 133 | ["\r\n", "1", "2", "3", "\r\n"]. You can join this array with 134 | join("") array method to get the full output.
135 | Optional options object may include stream property, which 136 | is false by default. When set to true, callback 137 | will be fired every time something is printed to the terminal simultaneously. 138 |
onUserInput(callback) 143 | callback(text, mode) is fired right after user presses enter. 144 | Argument text is a String of user input, and 145 | mode is a Number, which can be compared 146 | with one of the terminal mode constants, such as MODE_PROMPT. 147 |
removeCallback(callback) 152 | Remove any previously assigned callback. Any function which accepts callback 153 | returns it, and you can pass the callback here once you no longer need it to stop it 154 | from firing. 155 |
print(text) 160 | Prints text which can include special characters and 161 | escape sequences. This function is input-safe, and you can 162 | print event when terminal is requesting for input without 163 | disrupting input. In this case the input will reappear 164 | right after text printed. 165 |
168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 |
ConstantDescription
MODE_PROMPTRegular input (ObjectScript command)
MODE_SQLInput in SQL mode (SQL command)
MODE_READPrompt issued by ObjectScript read c command
MODE_READ_CHARPrompt issued by ObjectScript read *c command
MODE_SPECIALSpecial CWT's input (commands like /help, /config etc)
180 | 181 | The next example demonstrates a way to intercept terminal's input: 182 | 183 | ```js 184 | let iFrame = document.querySelector("#terminal"); 185 | 186 | function myInitHandler (terminal) { 187 | terminal.execute("set hiddenVariable = 7", { 188 | echo: false // the default is false, this is just a demo 189 | }); 190 | terminal.onUserInput((text, mode) => { 191 | if (mode !== terminal.MODE_PROMPT) 192 | return; 193 | terminal.print("\r\nYou've just entered the next command: " + text); 194 | }); 195 | terminal.onOutput((chunks) => { 196 | // If you "write 12", chunks are ["\r\n", "12", "\r\n"]. 197 | // If you "write 1, 2", chunks are ["\r\n", "1", "2", "\r\n"]. 198 | if (chunks.slice(1, -1).join("") === "duck") { // if the user enters: write "duck" 199 | alert(`You've found a secret phrase!`); 200 | } 201 | }); 202 | } 203 | 204 | // At first, handle iFrame load event. Note that the load handler won't work 205 | // if this code is executed at the moment when iFrame is already initialized. 206 | iFrame.addEventListener("load", function () { 207 | iFrame.contentWindow.onTerminalInit(myInitHandler); // handle terminal initialization 208 | }); 209 | ``` 210 | 211 | WebTerminal Project Development 212 | ------------------------------- 213 | 214 | We are glad to see anyone who want to contribute to Web Terminal development! Check our 215 | [developer's guide](http://intersystems-community.github.io/webterminal/#docs.5). 216 | 217 | To be short, the "hot start" is extremely easy. Having latest [Git](https://git-scm.com/) and 218 | [NodeJS](https://nodejs.org/en/) installed (tested on NodeJS v4-8), execute the following: 219 | 220 | ```sh 221 | git clone https://github.com/intersystems-community/webterminal 222 | cd webterminal # enter repository directory 223 | import # build & import the project. YOU NEED TO EDIT CONSTANTS IN THIS FILE FIRST 224 | ``` 225 | 226 | Now, in `build` folder you will find `WebTerminal-v*.xml` file. Every time your 227 | changes are ready to be tested, just run `import` again. 228 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Caché WEB Terminal 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/client/js/analytics/index.js: -------------------------------------------------------------------------------- 1 | import * as terminal from "../index"; 2 | import * as storage from "../storage"; 3 | 4 | const STORAGE_NAME = "terminal-guid"; 5 | 6 | function guid () { 7 | let g = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { 8 | let r = Math.random()*16|0,v=c=='x'?r:r&0x3|0x8; return v.toString(16); 9 | }); 10 | storage.set(STORAGE_NAME, g); 11 | return g; 12 | } 13 | 14 | export function collect (initData = {}) { 15 | 16 | let local = location.hostname === "localhost" || location.hostname === "127.0.0.1", 17 | page = local ? "Local" : "Remote"; 18 | 19 | window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date; 20 | 21 | ga("create", "/* @echo package.gaID */", !local ? "auto" : { 22 | clientId: initData["InstanceGUID"] || storage.get(STORAGE_NAME) || guid() 23 | }); 24 | ga("set", "appName", "WebTerminal"); 25 | ga("set", "appVersion", terminal.VERSION); 26 | ga("set", "screenName", page); 27 | if (initData["zv"]) ga("set", "appInstallerId", initData["zv"]); 28 | ga("send", "pageview", page); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/client/js/autocomplete/hint.js: -------------------------------------------------------------------------------- 1 | import * as output from "../output"; 2 | import * as caret from "../input/caret"; 3 | import { output as outputElement } from "../elements"; 4 | 5 | let MAX_HINTS = 5; 6 | 7 | function Hint () { 8 | 9 | this.visible = false; 10 | 11 | this.element = document.createElement("div"); 12 | this.element.className = "hintBox"; 13 | this.element.style.display = "none"; 14 | // this.nestedElement = document.createElement("div"); 15 | // this.element.appendChild(this.nestedElement); 16 | outputElement.appendChild(this.element); 17 | 18 | this.maxVariantLength = 10; 19 | this.displayUnder = true; 20 | this.displayInLine = true; 21 | this.lastSeek = 1; 22 | this.firstDisplay = true; 23 | 24 | /** 25 | * @type {string[]} 26 | */ 27 | this.variants = []; 28 | this.variant = 0; 29 | 30 | } 31 | 32 | Hint.prototype.add = function (hints = []) { 33 | 34 | if (!(hints instanceof Array)) 35 | hints = [hints]; 36 | 37 | this.reset(); 38 | this.variants = hints.map((h) => { 39 | if (h.length > this.maxVariantLength) 40 | this.maxVariantLength = h.length; 41 | return { value: h, element: null }; 42 | }); 43 | 44 | if (this.variants.length) { 45 | this.update(); 46 | this.show(); 47 | } else { 48 | this.hide(); 49 | } 50 | 51 | }; 52 | 53 | Hint.prototype.reset = function () { 54 | this.element.textContent = ""; 55 | this.variants = []; 56 | this.variant = 0; 57 | this.lastSeek = 0; 58 | this.maxVariantLength = 0; 59 | this.firstDisplay = true; 60 | }; 61 | 62 | Hint.prototype.get = function () { 63 | return (this.variants[this.variant] || {}).value || ""; 64 | }; 65 | 66 | Hint.prototype.next = function (counter = 1) { 67 | let oldVar = this.variant; 68 | this.variant = (this.variant + counter + this.variants.length) % this.variants.length; 69 | if (oldVar !== this.variant) 70 | this.lastSeek = this.variant - oldVar; 71 | this.updateVariants(); 72 | }; 73 | 74 | Hint.prototype.updateVariants = function () { 75 | 76 | if (!this.visible) 77 | return; 78 | 79 | let displayable = this.variants.slice(this.variant, this.variant + MAX_HINTS), 80 | property = this.displayUnder ? "top" : "bottom"; 81 | 82 | function displayed (e) { 83 | for (let a of displayable) { if (a.element === e) return a; } return null; 84 | } 85 | 86 | for (let n of this.element.childNodes) { 87 | if (displayed(n) || n.DECAY) 88 | continue; 89 | n.DECAY = true; 90 | n.style[property] = 91 | `${ parseFloat(n.style[property]) - this.lastSeek * output.SYMBOL_HEIGHT }px`; 92 | n.style.opacity = 0; 93 | setTimeout(() => { if (n.parentNode) n.parentNode.removeChild(n); }, 300); 94 | } 95 | 96 | for (let i = 0; i < displayable.length; i++) { 97 | let v = displayable[i]; 98 | if (!v.element || v.element.DECAY) { 99 | let seek = Math.sign(this.lastSeek) * Math.min(Math.abs(this.lastSeek), MAX_HINTS); 100 | v.element = document.createElement(`div`); 101 | v.element.textContent = v.value; 102 | if (!this.firstDisplay) 103 | v.element.style.opacity = 0; 104 | v.element.style[property] = `${ 105 | (i + seek) * output.SYMBOL_HEIGHT 106 | }px`; 107 | this.element.appendChild(v.element); 108 | } 109 | setTimeout(((v) => () => { 110 | v.style[property] = `${ i * output.SYMBOL_HEIGHT }px`; 111 | v.style.opacity = 1; 112 | })(v.element), 25); 113 | } 114 | 115 | }; 116 | 117 | Hint.prototype.update = function () { 118 | 119 | if (!this.visible) 120 | return; 121 | 122 | let linesNumber = output.getLinesNumber(), 123 | lineIndex = output.getTopLineIndex() + caret.getY(), 124 | x = caret.getX(), 125 | width = Math.min(this.maxVariantLength, Math.ceil(output.WIDTH / 2)), 126 | height = Math.min(MAX_HINTS, this.variants.length); 127 | 128 | this.displayInLine = x + width < output.WIDTH; 129 | this.displayUnder = lineIndex + height + 1 < Math.max(linesNumber, output.HEIGHT); 130 | // console.log(`Display under: ${ this.displayUnder }, in line: ${ this.displayInLine }`); 131 | this.element.style.top = `${ 132 | ((this.displayUnder ? lineIndex + 1: lineIndex - height) 133 | + (this.displayInLine ? this.displayUnder ? -1 : 1 : 0)) * output.SYMBOL_HEIGHT 134 | }px`; 135 | this.element.style.left = `${ Math.min(x, output.WIDTH - width) * output.SYMBOL_WIDTH }px`; 136 | this.element.style.width = `${ width * output.SYMBOL_WIDTH }px`; 137 | this.element.style.height = `${ height * output.SYMBOL_HEIGHT }px`; 138 | 139 | this.updateVariants(); 140 | 141 | this.firstDisplay = false; 142 | 143 | }; 144 | 145 | Hint.prototype.show = function () { 146 | if (this.visible) 147 | return; 148 | this.visible = true; 149 | this.update(); 150 | this.element.style.display = "block"; 151 | }; 152 | 153 | Hint.prototype.hide = function () { 154 | if (!this.visible) 155 | return; 156 | this.visible = false; 157 | this.element.style.display = "none"; 158 | }; 159 | 160 | export default new Hint(); -------------------------------------------------------------------------------- /src/client/js/autocomplete/index.js: -------------------------------------------------------------------------------- 1 | import { getAutomaton } from "../parser"; 2 | import { 3 | TYPE_ID, 4 | TYPE_CHAR 5 | } from "../parser/pushdownAutomaton"; 6 | import { onInit } from "../init"; 7 | import * as input from "../input"; 8 | import hint from "./hint"; 9 | import types from "./types"; 10 | 11 | export let CURRENT = 0; 12 | 13 | export function suggest (state, base = "") { 14 | const BEEN = [], 15 | automaton = getAutomaton(); 16 | // console.log(`Suggest state: ${state}, Substring: ${base}`); 17 | // console.log(`Suggesting from state ${ state } with "${ base }"`); 18 | // match null | TYPE_CHAR (multiple) | TYPE_ID (once) 19 | function collect (state, base, cls = null, type = null) { 20 | if (BEEN[state]) // prevent looping 21 | return []; 22 | BEEN[state] = true; 23 | let rule = automaton[state], 24 | arr = []; 25 | if (!rule) { 26 | console.error(`No state ${ state }`); 27 | return []; 28 | } 29 | for (let row of rule) { 30 | if (row[0] === null) { 31 | if (row[1] === 0) 32 | break; 33 | arr = arr.concat(collect(row[1], base)); 34 | break; 35 | } 36 | if (row[0] === true || row[0] === 0) 37 | continue; 38 | if (row[0].type !== TYPE_CHAR && row[0].type !== TYPE_ID) 39 | continue; 40 | if (row[0].type === TYPE_CHAR && type === null && row[0].value && row[0].value.value === ",") 41 | continue; // Do not suggest variants starting with comma 42 | if (cls && row[0].value.class && row[0].value.class !== cls 43 | || type && row[0].value.type && row[0].value.type !== type) 44 | continue; 45 | if (row[0].type === TYPE_CHAR) { 46 | if (base !== "" && ((typeof row[0].value === "string" 47 | ? row[0].value : row[0].value.value) || "").indexOf(base) !== 0) 48 | continue; 49 | if (row[1] === 0) 50 | continue; 51 | let a = collect( 52 | row[1], 53 | base.substr(1), 54 | row[0].value.class || cls, 55 | row[0].value.type || type 56 | ); 57 | for (let r of a) { 58 | if (r[0] && r[0].value) { // suggest only / ids with value 59 | arr.push([ row[0].value ].concat(r)); 60 | } 61 | } 62 | } 63 | if (row[0].type === TYPE_ID) { 64 | if (base !== "" && typeof row[0].value.value === "string") { 65 | if (row[0].value.value === base) 66 | break; 67 | if (row[0].value.value.indexOf(base) === 0) { 68 | arr.push([{ 69 | value: row[0].value.value.substr(base.length), 70 | class: row[0].value.class, 71 | type: row[0].value.type 72 | }]); 73 | } // else continue (default) 74 | } else { 75 | arr.push([ row[0].value ]); 76 | } 77 | } 78 | } 79 | return arr; 80 | } 81 | return collect(state, base); 82 | } 83 | 84 | onInit(() => input.onKeyDown((e) => { 85 | if (e.keyCode === 17) { // CTRL 86 | if (!hint.visible) 87 | return; 88 | hint.next(e.location === 2 ? -1 : 1); 89 | } else if (e.keyCode === 9) { // TAB 90 | e.preventDefault(); 91 | if (!hint.visible) 92 | return; 93 | let val = input.getValue(), 94 | pos = input.getCaretPosition(), 95 | v = hint.get(); 96 | input.setValue(val.substr(0, pos) + v + val.substr(pos), pos + v.length); 97 | } 98 | })); 99 | 100 | function addVariants (variants = []) { 101 | hint.add(variants); 102 | } 103 | 104 | export function showSuggestions (show, suggestions = [], collector = []) { 105 | 106 | let suggesting = false, 107 | current = ++CURRENT, 108 | staticSuggestions = []; 109 | 110 | hint.reset(); 111 | 112 | if (show) for (let row of suggestions) { 113 | if (row[0].value) { // text 114 | let s = row.map(e => e.value).join(""); 115 | if (s.length) { 116 | suggesting = true; 117 | staticSuggestions.push(s); 118 | } 119 | } else if (row[0].type && typeof types[row[0].type] === "function") { // type 120 | types[row[0].type](collector.slice(), (v) => { 121 | if (current !== CURRENT) 122 | return; 123 | addVariants(v); 124 | }); 125 | suggesting = true; 126 | } 127 | } 128 | 129 | if (staticSuggestions.length) 130 | hint.add(staticSuggestions); 131 | 132 | if (!suggesting || !show) { 133 | hint.hide(); 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /src/client/js/autocomplete/types.js: -------------------------------------------------------------------------------- 1 | import * as server from "../server"; 2 | import * as favorites from "../favorite"; 3 | 4 | function collectOfType (collector, type) { 5 | let arr = []; 6 | for (let i = collector.length - 1; i >= 0; i--) { 7 | if (collector[i].type !== type) 8 | break; 9 | arr.push(collector[i].value); 10 | } 11 | if (arr.length) 12 | collector.splice(collector.length - arr.length, arr.length); 13 | return arr.reverse().join(""); 14 | } 15 | 16 | export default { 17 | "classname": (collector, cb) => { 18 | let subStr = collectOfType(collector, "classname"); 19 | server.send("ClassAutocomplete", subStr, (d) => { 20 | if (!d || !(d.length > 0)) 21 | return; 22 | cb(d.split(",").map(s => { 23 | let dotPos = s.indexOf(".", subStr.length); 24 | return dotPos > 0 25 | ? s.substring(subStr.length, dotPos + 1) 26 | : s.substr(subStr.length); 27 | }).filter((s, i, arr) => arr[i - 1] ? arr[i - 1] !== s : true)); 28 | }); 29 | }, 30 | "global": (collector, cb) => { 31 | let subStr = collectOfType(collector, "global").substr(1); 32 | server.send("GlobalAutocomplete", subStr, (d) => { 33 | if (!d || !(d.length > 0)) 34 | return; 35 | cb(d.split(",").map(s => { 36 | let dotPos = s.indexOf(".", subStr.length); 37 | return dotPos > 0 38 | ? s.substring(subStr.length, dotPos + 1) 39 | : s.substr(subStr.length); 40 | }).filter((s, i, arr) => arr[i - 1] ? arr[i - 1] !== s : true)); 41 | }); 42 | }, 43 | "publicClassMember": (collector, cb) => { 44 | let subStr = collectOfType(collector, "publicClassMember"), 45 | cls = collectOfType(collector, "classname"); 46 | server.send("ClassMemberAutocomplete", { className: cls, part: subStr }, (d) => { 47 | if (!d || !(d.length > 0)) 48 | return; 49 | cb(d.split(",").map(s => s.substr(subStr.length))); 50 | }); 51 | }, 52 | "parameter": (collector, cb) => { 53 | let par = collectOfType(collector, "parameter").substr(1), // remove the "#" symbol 54 | cls = collectOfType(collector, "classname"); 55 | server.send("ParameterAutocomplete", { className: cls, part: par }, (d) => { 56 | if (!d || !(d.length > 0)) 57 | return; 58 | cb(d.split(",").map(s => s.substr(par.length))); 59 | }); 60 | }, 61 | "variable": (collector, cb) => { 62 | let v = collectOfType(collector, "variable"); 63 | server.send("LocalAutocomplete", null, (d) => { 64 | if (!d) 65 | return; 66 | cb(Object.keys(d).filter(s => s.indexOf(v) === 0 && s.length !== v.length) 67 | .map(s => s.substr(v.length))); 68 | }); 69 | }, 70 | "favorites": (collector, cb) => { 71 | let v = collectOfType(collector, "favorites"); 72 | cb(Object.keys(favorites.list()).filter(s => s.indexOf(v) === 0 && s.length !== v.length) 73 | .map(s => s.substr(v.length))); 74 | }, 75 | "member": (collector, cb) => { 76 | let mem = collectOfType(collector, "member"), 77 | v = collectOfType(collector, "variable"); 78 | if (!v) 79 | return; 80 | server.send("MemberAutocomplete", { variable: v, part: mem }, (d) => { 81 | if (!d) 82 | return; 83 | cb(d.split(",").filter(s => s.indexOf(mem) === 0).map(s => s.substr(mem.length))); 84 | }); 85 | }, 86 | "memberMethod": (collector, cb) => { 87 | let mem = collectOfType(collector, "memberMethod"), 88 | v = collectOfType(collector, "variable"); 89 | if (!v) 90 | return; 91 | server.send("MemberAutocomplete", { variable: v, part: mem, methodsOnly: 1 }, (d) => { 92 | if (!d) 93 | return; 94 | cb(d.split(",").filter(s => s.indexOf(mem) === 0).map(s => s.substr(mem.length))); 95 | }); 96 | }, 97 | "routine": (collector, cb) => { 98 | let subStr = collectOfType(collector, "routine"); 99 | server.send("RoutineAutocomplete", subStr, (d) => { 100 | if (!d || !(d.length > 0)) 101 | return; 102 | cb(d.split(",").filter(s => !/\.[0-9]+$/.test(s)).map(s => { 103 | let dotPos = s.indexOf(".", subStr.length); 104 | return dotPos > 0 105 | ? s.substring(subStr.length, dotPos + 1) 106 | : s.substr(subStr.length); 107 | }).filter((s, i, arr) => arr[i - 1] ? arr[i - 1] !== s : true)); 108 | }); 109 | } 110 | } -------------------------------------------------------------------------------- /src/client/js/config.js: -------------------------------------------------------------------------------- 1 | import * as storage from "./storage"; 2 | import * as locale from "./localization"; 3 | import * as server from "./server"; 4 | import * as output from "./output"; 5 | import { onInit } from "./init"; 6 | 7 | const STORAGE_NAME = `terminal-config`; 8 | const boolean = ["true", "false"], 9 | temps = {}, 10 | boolTransform = (a) => a === `true`, 11 | intTransform = (a) => parseInt(a); 12 | 13 | const metadata = { // those keys that are not listed in this object are invalid ones 14 | defaultNamespace: { 15 | default: "" 16 | }, 17 | initMessage: { 18 | default: true, 19 | values: boolean, 20 | transform: boolTransform 21 | }, 22 | language: { 23 | default: locale.suggestLocale(), 24 | values: locale.getLocales() 25 | }, 26 | maxHistorySize: { 27 | default: 200, 28 | transform: intTransform 29 | }, 30 | serverName: { 31 | default: "", 32 | global: true 33 | }, 34 | sqlMaxResults: { 35 | default: 777, 36 | transform: intTransform 37 | }, 38 | suggestions: { 39 | default: true, 40 | values: boolean, 41 | transform: boolTransform 42 | }, 43 | syntaxHighlight: { 44 | default: true, 45 | values: boolean, 46 | transform: boolTransform 47 | }, 48 | updateCheck: { 49 | default: true, 50 | values: boolean, 51 | transform: boolTransform, 52 | onSet: (v) => output.print(v ? ":)" : ":(") 53 | } 54 | }; 55 | 56 | let defaults = {}; 57 | for (let p in metadata) 58 | defaults[p] = metadata[p].default; 59 | 60 | let config = 61 | (Object.assign(Object.assign({}, defaults), (() => { 62 | let o = JSON.parse(storage.get(STORAGE_NAME)); 63 | for (let p in o) { if (!metadata.hasOwnProperty(p)) delete o[p]; } 64 | return o; 65 | })() || {})); 66 | 67 | onInit(() => locale.setLocale(config.language)); 68 | 69 | export function get (key) { 70 | return typeof temps[key] !== "undefined" ? temps[key] 71 | : typeof config[key] === "undefined" ? null : config[key]; 72 | } 73 | 74 | /** 75 | * @param {Set} updated 76 | */ 77 | function onUpdate (updated) { 78 | if (updated.has("language")) 79 | locale.setLocale(config.language); 80 | if (updated.has("serverName")) 81 | document.title = config.serverName; 82 | storage.set(STORAGE_NAME, JSON.stringify(config)); 83 | } 84 | 85 | /** 86 | * Set the configuration option. 87 | * @param {string} key 88 | * @param {*} value 89 | * @param {boolean} localOnly - Updates only the local values. 90 | * @returns {String} - Error message or an empty string if no errors happened. 91 | */ 92 | export function set (key, value, localOnly = false) { 93 | if (!metadata.hasOwnProperty(key)) 94 | return locale.get(`confNoKey`, key); 95 | if (metadata[key].values && metadata[key].values.indexOf(value) === -1) 96 | return locale.get(`confInvVal`, key, 97 | metadata[key].values.map(v => `\x1b[(constant)m${ v }\x1b[0m`).join(", ")); 98 | let v = metadata[key].transform ? metadata[key].transform(value) : value, 99 | oldConfig = config[key]; 100 | if (!localOnly && metadata[key] && metadata[key].global) { 101 | server.send(`${ key }ConfigSet`, v, (ok) => { 102 | if (ok === 1) 103 | return; 104 | config[key] = oldConfig; 105 | onUpdate(new Set([key])); 106 | }); 107 | } 108 | config[key] = v; 109 | if (metadata[key].onSet) 110 | metadata[key].onSet(v); 111 | onUpdate(new Set([key])); 112 | return ""; 113 | } 114 | 115 | /** 116 | * Sets the option only for the current session. 117 | * @param {string} key 118 | * @param {*} value 119 | */ 120 | export function setTemp (key, value) { 121 | if (!metadata.hasOwnProperty(key) 122 | || (metadata[key].values && metadata[key].values.indexOf(value) === -1)) 123 | return; 124 | temps[key] = metadata[key].transform ? metadata[key].transform(value) : value; 125 | } 126 | 127 | export function reset () { 128 | for (let p in config) { 129 | if (metadata[p] && !metadata[p].global) 130 | config[p] = metadata[p].default; 131 | } 132 | onUpdate(new Set(Object.keys(metadata))); 133 | } 134 | 135 | export function list () { 136 | let o = {}; 137 | for (let p in config) { 138 | o[p] = { 139 | value: config[p], 140 | global: !!metadata[p].global 141 | }; 142 | } 143 | return o; 144 | } -------------------------------------------------------------------------------- /src/client/js/elements.js: -------------------------------------------------------------------------------- 1 | import { images, onWindowLoad } from "./lib"; 2 | 3 | /** 4 | * DOM controller for terminal. Initializes all terminal elements. 5 | * 6 | * Terminal structure: 7 | *
- terminal window which will fill parent by 100% width and 100% height 8 | * - floating input 9 | *
10 | *
11 | * ... 12 | *
13 | *
14 | */ 15 | 16 | let e = (tag, cls) => { 17 | let t = document.createElement(tag); 18 | if (cls) 19 | t.className = cls; 20 | return t; 21 | }; 22 | 23 | export const terminal = e(`div`, `terminal`); 24 | export const output = e(`div`, `output`); 25 | export const input = e(`textarea`, `input`); 26 | export const themeLink = e(`link`); 27 | export const faviconLink = e(`link`); 28 | 29 | faviconLink.setAttribute(`rel`, `shortcut icon`); 30 | faviconLink.setAttribute(`href`,images.favicon); 31 | themeLink.setAttribute("rel", "stylesheet"); 32 | terminal.appendChild(output); 33 | 34 | onWindowLoad(() => { 35 | document.head.appendChild(faviconLink); 36 | document.head.appendChild(themeLink); 37 | document.body.appendChild(terminal); 38 | }); 39 | 40 | /* 41 | TerminalElements.prototype._initialize = function (parentElement) { 42 | 43 | var centralizer = document.createElement("div"), 44 | centralizerInner = document.createElement("div"); 45 | 46 | this.terminal.className = "terminalContainer"; 47 | centralizer.className = "terminalOutputCentralizer"; 48 | this.input.className = "terminalInput"; 49 | this.output.className = "terminalOutput"; 50 | 51 | this.terminal.appendChild(this.input); 52 | centralizerInner.appendChild(this.output); 53 | centralizer.appendChild(centralizerInner); 54 | this.terminal.appendChild(centralizer); 55 | parentElement.appendChild(this.terminal); 56 | 57 | this.themeLink.id = "terminal-theme"; 58 | this.themeLink.setAttribute("rel", "stylesheet"); 59 | this.terminal.appendChild(this.themeLink); 60 | 61 | }; 62 | */ -------------------------------------------------------------------------------- /src/client/js/favorite.js: -------------------------------------------------------------------------------- 1 | import * as storage from "./storage"; 2 | 3 | const STORAGE_NAME = `terminal-favorites`; 4 | 5 | let favorites = (JSON.parse(storage.get(STORAGE_NAME)) || {}); 6 | 7 | /** 8 | * Returns saved variant or an empty string. 9 | * @param {string} key 10 | * @returns {string} 11 | */ 12 | export function get (key) { 13 | return favorites[key] || ""; 14 | } 15 | 16 | /** 17 | * Saves a variant. 18 | * @param {string} key 19 | * @param {string} value 20 | * @returns {string} 21 | */ 22 | export function set (key, value) { 23 | favorites[key] = value; 24 | updateStorage(); 25 | } 26 | 27 | export function list () { 28 | return Object.assign({}, favorites); 29 | } 30 | 31 | /** 32 | * Clears a variant or clears all variants. 33 | * @param {string} [key] 34 | * @returns {boolean} 35 | */ 36 | export function clear (key) { 37 | let changed = false; 38 | if (key) { 39 | if (favorites.hasOwnProperty(key)) changed = true; 40 | delete favorites[key]; 41 | } else { 42 | if (Object.keys(favorites).length) changed = true; 43 | favorites = {}; 44 | } 45 | updateStorage(); 46 | return changed; 47 | } 48 | 49 | function updateStorage () { 50 | storage.set(STORAGE_NAME, JSON.stringify(favorites)); 51 | } -------------------------------------------------------------------------------- /src/client/js/index.js: -------------------------------------------------------------------------------- 1 | import "babel-polyfill"; 2 | import "./network"; 3 | import * as output from "./output"; 4 | import * as input from "./input"; 5 | import * as locale from "./localization"; 6 | import * as server from "./server"; 7 | import * as config from "./config"; 8 | import { initDone } from "./init"; 9 | import { get, getURLParams } from "./lib"; 10 | 11 | let onAuthHandlers = [], 12 | userInputHandlers = [], 13 | outputHandlers = [], 14 | bufferedOutput = [], 15 | inputIsActivated = false, 16 | AUTHORIZED = false, 17 | terminal = null; 18 | 19 | export const VERSION = "/* @echo package.version */"; 20 | 21 | export let NAMESPACE = "USER", 22 | MODE = Terminal.prototype.MODE_PROMPT; // PROMPT || SQL, other modes are emulated 23 | 24 | export function onAuth (callback) { 25 | if (AUTHORIZED) { 26 | callback(terminal); 27 | return; 28 | } 29 | onAuthHandlers.push(callback); 30 | } 31 | 32 | export function authDone () { 33 | if (AUTHORIZED) 34 | return; 35 | AUTHORIZED = true; 36 | onAuthHandlers.forEach(h => h(terminal)); 37 | } 38 | 39 | export const inputActivated = () => { 40 | inputIsActivated = true; 41 | if (bufferedOutput.length) { 42 | for (const handler of outputHandlers) { 43 | if (handler.stream) 44 | continue; 45 | handler.callback(bufferedOutput); 46 | } 47 | bufferedOutput = []; 48 | } 49 | }; 50 | 51 | export const onUserInput = (text, mode) => { 52 | userInputHandlers.forEach((h) => h(text, mode)); 53 | bufferedOutput = []; 54 | inputIsActivated = false; 55 | }; 56 | 57 | export const onOutput = (string) => { 58 | bufferedOutput.push(string); 59 | for (const handler of outputHandlers) { 60 | if (!handler.stream || inputIsActivated) 61 | continue; 62 | handler.callback([ string ]); 63 | } 64 | }; 65 | 66 | /** 67 | * Register the callback which will be executed right after terminal is initialized. This callback 68 | * is simultaneously triggered if WebTerminal initialization is already done. 69 | * @param {terminalInitCallback} callback 70 | */ 71 | window.onTerminalInit = onAuth; 72 | 73 | /** 74 | * WebTerminal's API object. 75 | * @author ZitRo 76 | */ 77 | export function Terminal () { 78 | 79 | initDone(this); 80 | 81 | } 82 | 83 | Terminal.prototype.MODE_PROMPT = 1; 84 | Terminal.prototype.MODE_SQL = 2; 85 | Terminal.prototype.MODE_READ = 3; 86 | Terminal.prototype.MODE_READ_CHAR = 4; 87 | Terminal.prototype.MODE_SPECIAL = 5; 88 | 89 | /** 90 | * Function accepts the callback, which is fired when user enter a command, character or a string. 91 | * @param {{ [stream]: boolean=false, [callback]: function }} [options] 92 | * @param {terminalOutputCallback} callback 93 | * @returns {function} - Your callback. 94 | */ 95 | Terminal.prototype.onOutput = function (options, callback) { 96 | if (!options || typeof options === "function") { 97 | callback = options || (() => { throw new Error("onOutput: no callback provided!"); }); 98 | options = {}; 99 | } 100 | if (typeof options.stream === "undefined") 101 | options.stream = false; 102 | options.callback = callback; 103 | outputHandlers.push(options); 104 | return callback; 105 | }; 106 | 107 | /** 108 | * Handles output both in stream or prompt mode. 109 | * @callback terminalOutputCallback 110 | * @param {string[]} - Output data presented as an array of string chunks. You can get the full 111 | * output as a single string by doing chunks.join(""). 112 | */ 113 | 114 | /** 115 | * Function accepts the callback, which is fired when user enter a command, character or a string. 116 | * @param {terminalUserEntryCallback} callback 117 | * @returns {function} - Your callback. 118 | */ 119 | Terminal.prototype.onUserInput = function (callback) { 120 | if (typeof callback !== "function") 121 | throw new Error("onUserInput: no callback provided!"); 122 | userInputHandlers.push(callback); 123 | return callback; 124 | }; 125 | 126 | /** 127 | * Handles user input. 128 | * @callback terminalUserEntryCallback 129 | * @param {String} text 130 | * @param {Number} mode 131 | */ 132 | 133 | /** 134 | * Print the text on terminal. 135 | * @param {string} text 136 | */ 137 | Terminal.prototype.print = function (text) { 138 | input.clearPrompt(); 139 | output.print(text); 140 | input.reprompt(); 141 | }; 142 | 143 | let executeCallback = null; 144 | 145 | /** 146 | * Print the text on terminal. 147 | * @param {string} command 148 | * @param {{ [echo]: boolean=false, [prompt]: boolean=false }} [options] 149 | * @param {function} [callback] 150 | */ 151 | Terminal.prototype.execute = function (command, options, callback) { 152 | if (typeof options === "function") { 153 | callback = options; 154 | } 155 | if (typeof options !== "object") { 156 | options = {}; 157 | } 158 | server.send("Execute", { 159 | command, 160 | echo: +(options.echo || 0), 161 | prompt: +(options.prompt || 0), 162 | bufferOutput: +(typeof callback === "function") 163 | }); 164 | if (typeof callback === "function") { 165 | return executeCallback = callback; 166 | } 167 | }; 168 | 169 | /** 170 | * Remove previously assigned callback. 171 | * @param {function} callback 172 | * @returns {boolean} - If callback was removed. 173 | */ 174 | Terminal.prototype.removeCallback = function (callback) { 175 | let i, 176 | deleted = false; 177 | if ((i = userInputHandlers.indexOf(callback)) !== -1) { 178 | userInputHandlers.splice(i, 1); 179 | return true; 180 | } 181 | if (typeof executeCallback === "function") { 182 | executeCallback = null; 183 | return true; 184 | } 185 | for (i = 0; i < outputHandlers.length; ++i) { 186 | if (outputHandlers[i].callback !== callback) 187 | continue; 188 | outputHandlers.splice(i, 1); 189 | --i; 190 | deleted = true; 191 | } 192 | return deleted; 193 | }; 194 | 195 | export function promptCallback (data) { 196 | if (typeof executeCallback === "function") { 197 | executeCallback([ data ]); 198 | executeCallback = null; 199 | } 200 | } 201 | 202 | function initialize () { 203 | let text = locale.get(`beforeInit`), 204 | urlParams = getURLParams(); 205 | terminal = new Terminal(); 206 | 207 | output.printLine(text); 208 | let ns = urlParams["NS"] || urlParams["ns"] || config.get("defaultNamespace"), 209 | urlPs = location.search.match(/ns=[^&]*/i) === null 210 | ? location.search === "" 211 | ? ns 212 | ? `?ns=${ encodeURIComponent(ns) }` 213 | : "" 214 | : location.search + (ns ? `&ns=${ encodeURIComponent(ns) }` : "") 215 | : ns 216 | ? location.search.replace(/ns=[^&]*/i, `ns=${ encodeURIComponent(ns) }`) 217 | : location.search; 218 | get("auth" + urlPs, (obj) => { 219 | if (!obj.key) { 220 | output.printLine(locale.get(`unSerRes`), JSON.stringify(obj)); 221 | return; 222 | } 223 | try { 224 | output.print(`\x1b[1;1H` + (new Array(text.length + 1)).join(` `) + `\x1b[1;1H`); 225 | server.connect({ 226 | key: obj.key 227 | }); 228 | } catch (e) { 229 | output.printLine(locale.get(`jsErr`, e.message)); 230 | } 231 | }); 232 | } 233 | 234 | window[addEventListener ? `addEventListener` : `attachEvent`]( 235 | addEventListener ? `load` : `onload`, 236 | initialize 237 | ); 238 | -------------------------------------------------------------------------------- /src/client/js/init.js: -------------------------------------------------------------------------------- 1 | let toCall = [], 2 | initialized = false, 3 | terminal = null; 4 | 5 | /** 6 | * Execute callback when application is ready. 7 | * @param {function} cb 8 | */ 9 | export function onInit (cb) { 10 | if (initialized) { 11 | cb(terminal); 12 | return; 13 | } 14 | toCall.push(cb); 15 | } 16 | 17 | /** 18 | * Triggered by WebTerminal when initialization is done. 19 | */ 20 | export function initDone (webTerminal) { 21 | terminal = webTerminal; 22 | initialized = true; 23 | toCall.forEach(f => f(terminal)); 24 | } 25 | 26 | /** 27 | * Handles WebTerminal's initialization. 28 | * @callback terminalInitCallback 29 | * @param {Terminal} terminal 30 | */ -------------------------------------------------------------------------------- /src/client/js/input/caret.js: -------------------------------------------------------------------------------- 1 | import { SYMBOL_WIDTH, SYMBOL_HEIGHT, getTopLineIndex, getCursorX, getCursorY } from "../output"; 2 | import { output as outputElement } from "../elements"; 3 | 4 | const CLASS_NAME = "caret"; 5 | const IS_IE = window.navigator.userAgent.indexOf("MSIE ") !== -1 6 | || window.navigator.userAgent.indexOf("Trident/") !== -1; 7 | 8 | /** 9 | * @type {HTMLElement} 10 | */ 11 | let element = document.createElement(`div`); 12 | element.className = CLASS_NAME; 13 | 14 | let x = 0, 15 | y = 0; 16 | 17 | export function getX () { 18 | return x; 19 | } 20 | 21 | export function getY () { 22 | return y; 23 | } 24 | 25 | /** 26 | * Updates the caret position and visibility. 27 | */ 28 | export function update () { 29 | 30 | element.style.left = 31 | `${ (x = getCursorX() - 1) * SYMBOL_WIDTH }px`; 32 | element.style.top = 33 | `${ getTopLineIndex() * SYMBOL_HEIGHT + (y = getCursorY() - 1) * SYMBOL_HEIGHT }px`; 34 | 35 | if (IS_IE) // do not show caret in older IE: it has it's own input caret 36 | return; 37 | 38 | if (!element.parentNode) 39 | outputElement.appendChild(element); 40 | 41 | } 42 | 43 | /** 44 | * Hides the caret. 45 | */ 46 | export function hide () { 47 | 48 | if (element.parentNode) 49 | element.parentNode.removeChild(element); 50 | 51 | } -------------------------------------------------------------------------------- /src/client/js/input/handlers.js: -------------------------------------------------------------------------------- 1 | import special from "./special"; 2 | import * as input from "./index"; 3 | import * as output from "../output"; 4 | import * as locale from "../localization"; 5 | import * as server from "../server"; 6 | import { Terminal } from "../index"; 7 | import * as terminal from "../index"; 8 | import * as config from "../config"; 9 | 10 | export default { 11 | special: (value, lexemes) => { 12 | terminal.onUserInput(value, Terminal.prototype.MODE_SPECIAL); 13 | let secondVal = (lexemes[1] || {}).value, 14 | result = undefined; 15 | input.clear(); 16 | if (typeof special[secondVal] === "function") { 17 | output.print(`\r\n`); 18 | result = special[secondVal](lexemes); 19 | output.print(`\r\n`); 20 | } else { 21 | if (typeof secondVal === "undefined") 22 | output.print(`\r\n${ locale.get(`askEnSpec`) }\r\n`); 23 | else 24 | output.print(`\r\n${ locale.get(`noSpecComm`, secondVal) }\r\n`); 25 | } 26 | if (result !== false) 27 | input.reprompt(); 28 | }, 29 | normal: (value, lexemes, mode) => { 30 | terminal.onUserInput(value, mode); 31 | }, 32 | sql: (value) => { 33 | let max = config.get(`sqlMaxResults`); 34 | terminal.onUserInput(value, Terminal.prototype.MODE_SQL); 35 | output.newLine(); 36 | server.send("SQL", { sql: value, max: max }, (data) => { 37 | if (data.error) { 38 | output.printLine(`\x1b[(wrong)m${ locale.parse(data.error) }\x1b[0m`); 39 | } else { 40 | data.headers = data.headers || []; 41 | data.data = data.data || []; 42 | let html = [``, data.data.slice(0, max).map(r => 44 | ``).join(``), 45 | ``, data.data.length >= max 46 | ? `` : data.data.length === 0 48 | ? `` : ``, `
`, data.headers.join(``), 43 | `
` + (r || []).join(``) + `
${ 47 | locale.get(`sqlMaxRows`, max) }
${ locale.get("sqlNoData") 49 | }
`]; 50 | output.printLine(`\r\n\x1b!${ html.join("") }`); 51 | } 52 | input.prompt(`${ terminal.NAMESPACE }:SQL > `); 53 | }); 54 | } 55 | } -------------------------------------------------------------------------------- /src/client/js/input/history.js: -------------------------------------------------------------------------------- 1 | import * as storage from "../storage"; 2 | import * as config from "../config"; 3 | 4 | const STORAGE_NAME = `terminal-history`; 5 | 6 | let history = (JSON.parse(storage.get(STORAGE_NAME)) || [""]), 7 | current = history.length - 1; 8 | 9 | /** 10 | * Get the history variant relative to the current. 11 | * @param {number} increment - Return previous or next history variant. 12 | * @returns {string} 13 | */ 14 | export function get (increment = 0) { 15 | if (increment && history.length) 16 | current = (current + history.length + increment) % history.length; 17 | return history[current] || ""; 18 | } 19 | 20 | /** 21 | * Set the latest history state to string. 22 | * @param {string} string 23 | */ 24 | export function set (string) { 25 | let c = history.length - 1; 26 | history[c < 0 ? 0 : c] = string; 27 | } 28 | 29 | /** 30 | * Returns if the current history state is the last one. 31 | * @returns {boolean} 32 | */ 33 | export function isLast () { 34 | return current === history.length - 1; 35 | } 36 | 37 | export function setLast (string = "") { 38 | let c = history.length - 1; 39 | current = c < 0 ? 0 : c; 40 | if (string) 41 | history[current] = string; 42 | } 43 | 44 | /** 45 | * Add a record to the history. 46 | * @param {string} string 47 | */ 48 | export function push (string) { 49 | if (!string || history[history.length - 2] === string) { 50 | current = history.length - 1; 51 | return; 52 | } 53 | history[history.length - 1] = string; 54 | history.push(""); 55 | current = history.length - 1; 56 | save(); 57 | } 58 | 59 | function save () { 60 | storage.set(STORAGE_NAME, JSON.stringify(history.slice(-config.get("maxHistorySize")))); 61 | } -------------------------------------------------------------------------------- /src/client/js/input/special.js: -------------------------------------------------------------------------------- 1 | import * as locale from "../localization"; 2 | import * as output from "../output"; 3 | import * as input from "../input"; 4 | import * as config from "../config"; 5 | import * as terminal from "../index"; 6 | import * as server from "../server"; 7 | import * as tracing from "../tracing"; 8 | import * as favorite from "../favorite"; 9 | import { prompt } from "../server/handlers"; 10 | import { Terminal } from "../index"; 11 | import { checkUpdate } from "../network"; 12 | 13 | function tableObject (keyHeader, valHeader, object) { 14 | let longest = keyHeader.length + 2; 15 | for (let p in object) if (p.length > longest) longest = p.length; 16 | output.print( 17 | `\x1b[1m${ keyHeader }\x1b[0m\x1b[${ longest }G \x1b[1m${ valHeader }\x1b[0m\r\n` 18 | ); 19 | for (let p in object) { 20 | output.print(`\x1b[(constant)m${ p }\x1b[0m\x1b[${ longest }G= ${ object[p] }\r\n`); 21 | } 22 | } 23 | 24 | /** 25 | * Special commands handler. Each key of this object is a command of type "/". 26 | * Please, keep items ordered alphabetically. 27 | */ 28 | export default { 29 | "help": () => { 30 | output.print(locale.get(`help`) + `\r\n`); 31 | }, 32 | "clear": () => { 33 | output.reset(); 34 | }, 35 | "config": (strings) => { 36 | 37 | let out = (list, pad, global) => { 38 | for (let p in list) { 39 | if (list[p].global !== global) 40 | continue; 41 | let val = typeof list[p].value === "string" && list[p].value.length === 0 42 | ? `\x1b[(string)m""\x1b[0m` 43 | : `\x1b[(constant)m${ list[p].value }\x1b[0m`; 44 | output.print(`\x1b[(variable)m${ p }\x1b[0m\x1b[${ pad }G= ${ val }\r\n`); 45 | } 46 | }; 47 | 48 | if (strings.length < 4) { 49 | let list = config.list(), 50 | longest = 0; 51 | for (let p in list) if (p.length > longest) longest = p.length; 52 | longest += 2; 53 | output.print(`${ locale.get(`availConfLoc`) }\r\n`); 54 | out(list, longest, false); 55 | output.print(`${ locale.get(`availConfGlob`) }\r\n`); 56 | out(list, longest, true); 57 | output.print(`${ locale.get(`confHintSet`) }\r\n`); 58 | return; 59 | } 60 | let defaults = strings.filter(v => v.class === "global")[0]; 61 | if (defaults) { 62 | config.reset(); 63 | return; 64 | } 65 | if (strings.length < 5) { 66 | output.print(`${ locale.get(`confHintSet`) }\r\n`); 67 | return; 68 | } 69 | let key = strings.filter(v => v.class === "variable")[0], 70 | val = strings.filter(v => v.class === "constant" || v.class === "string")[0]; 71 | if (!key || !val) { 72 | output.print(`${ locale.get(`confHintSet`) }\r\n`); 73 | return; 74 | } 75 | let res = config.set( 76 | key.value, 77 | val.class === "string" ? val.value.substr(1, val.value.length - 2) : val.value 78 | ); 79 | if (res !== "") 80 | output.print(`${ res }\r\n`); 81 | }, 82 | "favorite": (chain) => { 83 | if (chain.length < 4) { 84 | let list = favorite.list(); 85 | if (Object.keys(list).length > 0) { 86 | tableObject(locale.get(`favKey`), locale.get(`favVal`), list); 87 | output.newLine(); 88 | } 89 | output.print(locale.get(`favDesc`) + `\r\n`); 90 | return; 91 | } 92 | if (chain[3].value === "delete") { 93 | if (chain[4] && chain[4].value === " " && chain[5]) { 94 | let result = favorite.clear(chain[5].value); 95 | output.print(locale.get(`favDel${ result ? "OK" : "NotOK" }`, chain[5].value) 96 | + `\r\n`); 97 | } else { 98 | favorite.clear(); 99 | output.print(locale.get(`favDel`) + `\r\n`); 100 | } 101 | return; 102 | } 103 | if (chain.length < 6) { 104 | let val = favorite.get(chain[3].value); 105 | if (val) 106 | setTimeout(() => input.setValue(val), 1); 107 | else 108 | output.print(locale.get(`noFav`, chain[3].value) + `\r\n`); 109 | } 110 | let s = chain.slice(5).map(e => e.value).join(""); 111 | if (s && chain[3].value) { 112 | favorite.set(chain[3].value, s); 113 | output.print(locale.get(`favSet`, chain[3].value) + `\r\n`); 114 | } 115 | }, 116 | "info": () => { 117 | output.print(locale.get(`info`) + `\r\n`); 118 | }, 119 | "logout": () => { 120 | let outcome; 121 | try { outcome = document.execCommand("ClearAuthenticationCache") } catch(e) {} 122 | if (!outcome) { 123 | outcome = ((x) => { 124 | if (!x) return; 125 | if (x) { 126 | x.open("HEAD", location.href, true, "logout", 127 | (new Date()).getTime().toString()); 128 | x.send(""); 129 | return true; 130 | } else { 131 | return false; 132 | } 133 | })(window.XMLHttpRequest 134 | ? new window.XMLHttpRequest() 135 | : ( window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : null )) 136 | } 137 | if (!outcome) { 138 | output.printLine(locale.get("unLogOut")); 139 | } else { 140 | output.printLine(locale.get("logOut")); 141 | location.reload(); 142 | } 143 | }, 144 | "sql": () => { 145 | let sql = terminal.MODE !== Terminal.prototype.MODE_SQL; 146 | terminal.MODE = !sql ? Terminal.prototype.MODE_PROMPT : Terminal.prototype.MODE_SQL; 147 | if (sql) 148 | input.prompt(`${ terminal.NAMESPACE }:SQL > `); 149 | else 150 | prompt(terminal.NAMESPACE); 151 | return false; 152 | }, 153 | "trace": (strings) => { 154 | if (strings.length < 4) { 155 | output.print(locale.get(`tracingUsage`) + `\r\n`); 156 | let list = tracing.getList(); 157 | if (list) { 158 | output.print(locale.get(`traceSight`, list) + `\r\n`); 159 | } 160 | return; 161 | } 162 | if (strings[3].value === "stop") { 163 | server.send("StopTracing", {}, (res = { OK: false }) => { 164 | if (res.OK) tracing.stop(); 165 | output.printAsync(`${ locale.get(res.OK ? "traceStopOK" : "traceStopNotOK") }\r\n`); 166 | }); 167 | return; 168 | } 169 | let watchFor = strings.slice(3).map(e => e.value).join(""); 170 | server.send("Trace", watchFor, (res = { OK: 0 }) => { 171 | if (!res.OK) { 172 | output.printAsync(`${ locale.get("traceBad", watchFor) }\r\n`); 173 | return; 174 | } 175 | if (res["started"]) { 176 | tracing.start(watchFor); 177 | output.printAsync(`${ locale.get("traceStart", watchFor) }\r\n`); 178 | return; 179 | } 180 | if (res["stopped"]) { 181 | tracing.stop(watchFor); 182 | output.printAsync(`${ locale.get("traceStop", watchFor) }\r\n`); 183 | } 184 | }); 185 | }, 186 | "update": () => { 187 | checkUpdate(true); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/client/js/lib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a new spliced string. 3 | * 4 | * @param {number} position 5 | * @param {number} length 6 | * @param {string} string 7 | * @returns {string} 8 | */ 9 | String.prototype.splice = function(position, length, string) { 10 | return (this.slice(0,position) + string + this.slice(position + Math.abs(length))); 11 | }; 12 | 13 | export function onWindowLoad (f) { 14 | if (window.addEventListener) { 15 | window.addEventListener(`load`, f) 16 | } else { 17 | window.attachEvent(`onload`, f) 18 | } 19 | } 20 | 21 | /** 22 | * Gets data from server and handles it. 23 | * 24 | * @param url 25 | * @param {function} callback 26 | */ 27 | export function get (url, callback) { 28 | 29 | var request = new XMLHttpRequest(); 30 | 31 | request.onreadystatechange = () => { 32 | 33 | if (request.readyState === 4) { 34 | 35 | if (request.status === 200) { 36 | let p = { error: "Parse error", data: request.responseText }; 37 | try { p = JSON.parse(request.responseText) } catch (e) {} 38 | callback(p); 39 | } else { 40 | callback({ error: `HTTP ${ request.status } error` }); 41 | } 42 | 43 | } 44 | 45 | }; 46 | 47 | try { 48 | request.open("GET", `${ url }${ url.indexOf("?") === -1 ? "?" : "&" }_=${ 49 | new Date().getTime() }`, true); 50 | request.send(); 51 | } catch (e) { 52 | // huh? 53 | console.error(e); 54 | } 55 | 56 | } 57 | 58 | export function getURLParams () { 59 | var query_string = {}; 60 | var query = window.location.search.substring(1); 61 | var vars = query.split("&"); 62 | for (var i=0; i < vars.length; i++) { 63 | var pair = vars[i].split("="); 64 | if (typeof query_string[pair[0]] === "undefined") { 65 | query_string[pair[0]] = decodeURIComponent(pair[1]); 66 | } else if (typeof query_string[pair[0]] === "string") { 67 | query_string[pair[0]] = [ query_string[pair[0]], decodeURIComponent(pair[1]) ]; 68 | } else { 69 | query_string[pair[0]].push(decodeURIComponent(pair[1])); 70 | } 71 | } 72 | return query_string; 73 | } 74 | 75 | export const images = { 76 | favicon: "data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAD///8BAAAABQAAABEAAAAJAAAAAwAAAAMAAAAD////Af///wH///8B////Af///wH///8B////Af///wH///8B////AQAAAAcAAAAVLyoiXzcxJa09NijfPjcp4YiHhcWbm5ypioqLhWtrbF8vLy85AAAAIQAAAAP///8B////AXt7fBGFhYZHiomLRXFua0FEPzNPOjMkY1dSSceqqqr/vLy9/7a2t/+vr7D/kZGS4wAAABkAAAAbAAAAFf///wGXlZKPq6qg57GtlK2vqIqjsaySt6+qlcOvr5/1t7es/76+t//GxsT/zMzM98fIyIW8vLxDwcHBKY6Njg////8BioZ8j5aTjpkUERGjHRIS4SIVFM8oGRa9Mh8ZrUc0KatfTT2tdWROsYZ1XauciHGVt6aQl7uwnp/GxLPRuLi2gYyIfo+XlJCZCQgIzQAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8IBgX/Eg8O5bOwpXuOi4CPmZaSmQgIB80AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wICAfGqqKB3kY2Cj5yZlJkIBwfNAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8FBATtpaKcd5SQhY+em5eZCAcHzQAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wUFBf8aGhr/Li0s65+dl3WXk4iPop+amQgHB80AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AQEB/xISEv8mJib/MjIy/zk3NumamJJ1m5eMj6WinZkHBwfPXV1d/wcHB/9PT0//V1dX/0tLS/8AAAD/BQUF/xkZGf8nJyf/MjIy/zw8PP9CPz3nlJKNdZ+bj5GopZ+ZBwcHz25ubv+qqqr/ODg4/zg4OP8pKSn/CAgI/xwcHP8oKCj/MzMz/zw8PP9DQ0P/SENB55COiXOin5KRq6iimQcHBs9gYGD/m5ub/wAAAP8AAAD/DQ0N/x0dHf8oKCj/MzMz/z09Pf9DQ0P/R0dH/0pFQ+WMioVzpaGUkaunoZ8ICAjPc3Nz/wcHB/0GBgb1FBMT7x4eHecqKSfhNTMx3T89OttIRUHZT0xH2VVSTNtwbWTflZOQibKwrYe4tanplpGCs4uDcamblIOto52Ot6Gcj7+fmo69nZmNs5qWi6mVkombjoyFjYaFgH9/f3xxfXx9Y3t8fi+npqYbsbKyU7m4uE2/v787srKxKY+OjxdtbW0H////Af///wH///8B////Af///wH///8B////Af///wH///8B//8AAPA/AAD8DwAAAA8AAAAAAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAAAAAAADwAA//8AAA==" 77 | }; -------------------------------------------------------------------------------- /src/client/js/localization/index.js: -------------------------------------------------------------------------------- 1 | import * as storage from "../storage"; 2 | import { printLine } from "../output"; 3 | import dictionary from "./dictionary"; 4 | 5 | /** 6 | * All available locales. 7 | * @type {String[]} - Two-letter code of the locale. 8 | */ 9 | export let LOCALES = []; 10 | 11 | let STORAGE_NAME = "terminal-localization", 12 | DEFAULT_LOCALE = "en", 13 | LEXEME_REGEX = /%([a-zA-Z0-9]+)(?:\(([^)]*)\))?/g, 14 | CURRENT_LOCALE = suggestLocale(); // overridden by config 15 | 16 | export function getLocales () { 17 | return (() => { 18 | let locales = []; 19 | //noinspection LoopStatementThatDoesntLoopJS 20 | for (let a in dictionary) { 21 | for (let b in dictionary[a]) { 22 | locales.push(b); 23 | } 24 | return locales; 25 | } 26 | })(); 27 | } 28 | 29 | function lexemeDefined (str) { 30 | if (str === "s" || str === "d") return false; 31 | return dictionary.hasOwnProperty(str); 32 | } 33 | 34 | export function suggestLocale () { 35 | let lang = navigator.language; 36 | if (!LOCALES.length) 37 | LOCALES = getLocales(); 38 | if (LOCALES.indexOf(lang) === -1) { 39 | lang = DEFAULT_LOCALE; 40 | } 41 | return lang; 42 | } 43 | 44 | export function getLocale () { 45 | 46 | let lang = storage.get(STORAGE_NAME); 47 | 48 | if (!LOCALES.length) 49 | LOCALES = getLocales(); 50 | 51 | if (!lang || LOCALES.indexOf(lang) === -1) { 52 | lang = navigator.language; 53 | if (LOCALES.indexOf(lang) === -1) { 54 | lang = DEFAULT_LOCALE; 55 | } 56 | } 57 | 58 | return lang; 59 | 60 | } 61 | 62 | export function setLocale (code) { 63 | 64 | if (LOCALES.indexOf(code) !== -1) { 65 | CURRENT_LOCALE = code; 66 | storage.set(STORAGE_NAME, code); 67 | return true; 68 | } else { 69 | printLine(get(`noLocale`, code)); 70 | return false; 71 | } 72 | 73 | } 74 | 75 | /** 76 | * Get localized text. 77 | * @param locId - Translation id. 78 | * @param args - Any arguments that will be inserted to translation (instead of %s or %n) 79 | * @returns {String} 80 | */ 81 | export function get (locId, ...args) { 82 | 83 | let i = -1; 84 | 85 | return ( 86 | (dictionary[locId]) 87 | ? dictionary[locId][CURRENT_LOCALE] || `[${ locId }.${ CURRENT_LOCALE }?]` 88 | : `[${ locId }?]` 89 | ).replace(/%[sn]/g, function (part) { 90 | return typeof args[++i] !== "undefined" 91 | ? ( part.charAt(1) === "s" ? args[i].toString() : parseFloat(args[i]) ) 92 | : part; 93 | }); 94 | 95 | } 96 | 97 | export function parse (string, ...parameters) { 98 | 99 | let i = 0; 100 | return (string + "").replace(LEXEME_REGEX, (match, prop, args) => { 101 | return lexemeDefined(prop) 102 | ? get.apply( 103 | window, 104 | [prop].concat( 105 | args 106 | ? args.split(",").map( (str) => parse(str) ) 107 | : undefined 108 | ) 109 | ) 110 | : typeof parameters[i] !== "undefined" ? parameters[i++] // place parameter 111 | : match; // return match if nothing found 112 | }); 113 | 114 | } -------------------------------------------------------------------------------- /src/client/js/network/index.js: -------------------------------------------------------------------------------- 1 | import { get } from "../lib"; 2 | import * as input from "../input"; 3 | import * as terminal from "../index"; 4 | import * as output from "../output"; 5 | import * as locale from "../localization"; 6 | import "./update"; 7 | import * as config from "../config"; 8 | 9 | if (config.get(`updateCheck`)) { 10 | checkUpdate(); 11 | } 12 | 13 | export function checkUpdate (force = false) { 14 | get("https://intersystems-community.github.io/webterminal/terminal.json", (data = {}) => { 15 | if (data.error || typeof data[`motd`] === "undefined") 16 | return; 17 | terminal.onAuth(() => handleNetworkData(data, force)); 18 | }); 19 | } 20 | 21 | function handleNetworkData (data, force = false) { 22 | if (!config.get(`updateCheck`) && !force) { 23 | return; 24 | } 25 | input.clearPrompt(); 26 | if (data[`motd`] && config.get(`initMessage`)) 27 | output.printLine(data[`motd`]); 28 | if (data[`versions`] instanceof Array) 29 | printUpdate(data[`versions`]); 30 | input.reprompt(); 31 | } 32 | 33 | /** 34 | * Parses an array received from WebTerminal's home server. 35 | * @param versions 36 | */ 37 | function printUpdate (versions) { 38 | let changes = [], 39 | hiVersion = "", 40 | updateURL = ""; 41 | versions.forEach((version) => { 42 | // console.log(version.v, terminal.VERSION, versionGT(version.v, terminal.VERSION)); 43 | if (!versionGT(version.v, terminal.VERSION)) 44 | return; 45 | (version[`changes`] || []).forEach((c, i) => changes.push( 46 | "\x1b[2m" 47 | + ((i === 0 ? version.v : "") + `${ i === 0 ? ":" : "" } `) 48 | .substring(0, 16) + "\x1b[0m" 49 | + (typeof c === `string` ? c : c[`text`] || "") 50 | )); 51 | if (versionGT(version.v, hiVersion)) { 52 | hiVersion = version.v; 53 | updateURL = version.url; 54 | } 55 | }); 56 | if (!changes.length) 57 | return; 58 | output.printLine(locale.get( 59 | `updReady`, 60 | `javascript:window.updateTerminal("${ hiVersion }","${ updateURL }")` // no spaces here! 61 | )); 62 | if (!updateURL) { 63 | output.printLine( 64 | locale.get( 65 | `noUpdUrl`, `https://intersystems-community.github.io/webterminal/#downloads` 66 | ) 67 | ); 68 | return; 69 | } 70 | output.printLine(changes.join(`\r\n`)); 71 | } 72 | 73 | /** 74 | * Semantic versioning versions compare. 75 | * @param {string} high 76 | * @param {string} low 77 | * @returns {boolean} 78 | */ 79 | function versionGT (high, low) { 80 | let v1 = high.split(/[\-\.]/g), 81 | v2 = low.split(/[\-\.]/g); 82 | for (let i = 0; i < v1.length; i++) { 83 | if (isNaN(+v1[i]) || isNaN(+v2[i])) { 84 | if (v1[i] > v2[i]) 85 | return true; 86 | else if (v1[i] < v2[i]) 87 | return false; 88 | } else { 89 | if (+v1[i] > +v2[i]) 90 | return true; 91 | else if (+v1[i] < +v2[i]) 92 | return false; 93 | } 94 | } 95 | return v2.length > v1.length; 96 | } -------------------------------------------------------------------------------- /src/client/js/network/update.js: -------------------------------------------------------------------------------- 1 | import * as terminal from "../index"; 2 | import * as output from "../output"; 3 | import * as input from "../input"; 4 | import * as locale from "../localization"; 5 | import * as server from "../server"; 6 | 7 | let UPDATING = false; 8 | 9 | window.updateTerminal = function (version, url) { 10 | if (UPDATING) 11 | return; 12 | url = url.replace(/^http:/, "https:"); 13 | UPDATING = true; 14 | input.hideInput(); 15 | output.printLine(locale.get(`updStart`, terminal.VERSION, version)); 16 | output.printLine(`URL: ${ url }`); 17 | output.printLine(locale.get(`rSerUpd`)); 18 | server.send(`Update`, url); 19 | }; -------------------------------------------------------------------------------- /src/client/js/output/Line.js: -------------------------------------------------------------------------------- 1 | import { SYMBOL_HEIGHT, WIDTH, GRAPHIC_PROPERTIES } from "./index"; 2 | import * as elements from "../elements"; 3 | 4 | export const LINE_CLASS_NAME = `line`; 5 | 6 | /** 7 | * Output line used as instance for rendering terminal content. 8 | * 9 | * @param {number} index - Line top index. 10 | * @constructor 11 | */ 12 | export function Line (index) { 13 | 14 | /** 15 | * @type {HTMLElement} 16 | * @private 17 | */ 18 | this._lineElement = document.createElement("div"); 19 | this._lineElement.className = `line`; 20 | 21 | /** 22 | * Text of line which will be rendered. 23 | * 24 | * @private 25 | * @type {string} 26 | */ 27 | this.text = ""; 28 | 29 | /** 30 | * Line index. 31 | * @type {number} 32 | */ 33 | this.INDEX = index; 34 | 35 | this.HTML_LINE = false; 36 | this.HTMLRendered = false; 37 | 38 | /** 39 | * Array of graphic properties. The null value symbolizes that reset is needed. 40 | * @type {(Number|{class:String,style:String,tag:string,attrs:Object[]})[]} 41 | */ 42 | this.graphicProperties = []; 43 | 44 | this.setIndex(index); 45 | this._lineElement.style.height = `${ SYMBOL_HEIGHT }px`; 46 | elements.output.appendChild(this._lineElement); 47 | 48 | } 49 | 50 | /** 51 | * @param {number} index - Line top index. 52 | */ 53 | Line.prototype.setIndex = function (index) { 54 | this.INDEX = index; 55 | this._lineElement.setAttribute("index", index); 56 | this._lineElement.style.top = `${ index * SYMBOL_HEIGHT }px`; 57 | }; 58 | 59 | function getElement (gp, text) { 60 | let classes = ["g"], 61 | styles = [], 62 | tag = "span", 63 | attrs = []; 64 | for (let i = 0; i < gp.length; i += 2) { 65 | if (gp[i + 1].class) 66 | classes.push(gp[i + 1].class); 67 | if (gp[i + 1].style) 68 | styles.push(gp[i + 1].style); 69 | if (gp[i + 1].attrs) 70 | for (let a in gp[i + 1].attrs) 71 | attrs.push([a, gp[i + 1].attrs[a]]); 72 | if (gp[i + 1].tag) 73 | tag = gp[i + 1].tag; 74 | } 75 | let el = document.createElement(tag); 76 | el.className = classes.join(" "); 77 | if (styles.length) 78 | el.setAttribute("style", styles.join(";")); 79 | for (let a of attrs) 80 | el.setAttribute(a[0], a[1]); 81 | el.textContent = text; 82 | return el; 83 | } 84 | 85 | /** 86 | * Start treating line content as html. Removes it's actual content and replaces with "content". 87 | * @param content 88 | */ 89 | Line.prototype.setHTML = function (content) { 90 | this.HTML_LINE = true; 91 | this.text = content; 92 | this._lineElement.className = `html line`; 93 | this.HTMLRendered = false; 94 | this.render(); 95 | this._lineElement.style.maxHeight = 96 | `${ Math.max(SYMBOL_HEIGHT, this._lineElement.offsetHeight) }px`; 97 | }; 98 | 99 | Line.prototype.getHeight = function () { 100 | return this._lineElement.offsetHeight; 101 | }; 102 | 103 | /** 104 | * Renders text to html. 105 | */ 106 | Line.prototype.render = function () { 107 | 108 | if (this.HTML_LINE) { 109 | if (this.HTMLRendered) 110 | return; 111 | this._lineElement.innerHTML = this.text; 112 | this.HTMLRendered = true; 113 | return; 114 | } 115 | 116 | let tempDisplay = this._lineElement.style.display; 117 | this._lineElement.style.display = "none"; 118 | this._lineElement.innerHTML = ""; 119 | 120 | let lastI = 0; 121 | for (let i = 0; i < this.text.length; i++) { 122 | if (typeof this.graphicProperties[i] === "undefined") 123 | continue; 124 | if (lastI !== i) 125 | this._lineElement.appendChild( 126 | getElement(this.graphicProperties[lastI] || [], this.text.substring(lastI, i)) 127 | ); 128 | lastI = i; 129 | } 130 | if (this.text.length > 0) 131 | this._lineElement.appendChild( 132 | getElement(this.graphicProperties[lastI] || [], this.text.substr(lastI)) 133 | ); 134 | 135 | // console.log(`Rendered with`, JSON.parse(JSON.stringify(this.graphicProperties))); 136 | 137 | this._lineElement.style.display = tempDisplay; 138 | 139 | }; 140 | 141 | function collect (posArr, from = 0, to, init = [], clean = false) { 142 | let arr = init; 143 | for (let i = from; i < to; i++) { 144 | if (typeof posArr[i] === "undefined") 145 | continue; 146 | arr = posArr[i]; 147 | if (clean) 148 | posArr[i] = undefined; 149 | } 150 | return arr; 151 | } 152 | 153 | function equal (arr1, arr2) { 154 | if (arr1.length !== arr2.length) 155 | return false; 156 | for (let i = 0; i < arr1.length; i++) 157 | if (arr1[i] !== arr2[i]) 158 | return false; 159 | return true; 160 | } 161 | 162 | /** 163 | * Writes plain text to line starting from position. If line overflows, overflowing text will be 164 | * returned. 165 | * 166 | * @param {string} text - Bare text without any non-character symbols. Any html character 167 | * will be replaced with matching entities. 168 | * @param {number} [startPos] - Position to insert text to. 169 | * @returns {string} 170 | */ 171 | Line.prototype.print = function (text, startPos = this.text.length) { 172 | 173 | if (this.HTML_LINE) { 174 | this.HTML_LINE = false; 175 | this._lineElement.className = `line`; 176 | this._lineElement.style.maxHeight = ``; 177 | this.text = ``; 178 | } 179 | 180 | let part = text.substr(0, WIDTH - startPos), 181 | endPos = startPos + part.length; 182 | 183 | let before = collect(this.graphicProperties, 0, startPos, []), 184 | then = collect(this.graphicProperties, startPos, endPos, before, true); 185 | 186 | // console.log(`Printing from ${ startPos } ${ part.length } characters. Before:`, JSON.parse(JSON.stringify(before)), `Then:`, JSON.parse(JSON.stringify(then))); 187 | 188 | if (!equal(before, GRAPHIC_PROPERTIES)) 189 | this.graphicProperties[startPos] = GRAPHIC_PROPERTIES.slice(); 190 | // console.log(`( ${then.length}|| ${GRAPHIC_PROPERTIES.length}) && !${equal(then, GRAPHIC_PROPERTIES)} && ${position} + ${part.length} < ${text.length}`, JSON.parse(JSON.stringify(collect(this.graphicProperties, position + part.length, position + part.length + 1, then)))); 191 | if ((then.length || GRAPHIC_PROPERTIES.length) && !equal(then, GRAPHIC_PROPERTIES) && endPos < this.text.length) { 192 | this.graphicProperties[endPos] = collect(this.graphicProperties, endPos, endPos + 1, then); 193 | if (GRAPHIC_PROPERTIES.length === 0 && this.graphicProperties[endPos].length === 0) 194 | this.graphicProperties[endPos] = undefined; 195 | } 196 | 197 | // console.log(`line GP now`, this.graphicProperties); 198 | 199 | this.text = startPos < this.text.length 200 | ? this.text.substring(0, startPos) + part + this.text.substr(endPos) 201 | : this.text + new Array(startPos - this.text.length + 1).join(" ") + part; 202 | //this.render(); 203 | return part.length === text.length ? "" : text.substr(part.length); 204 | 205 | }; 206 | 207 | /** 208 | * Erases line. This function is much faster than rendering line with whitespaces. 209 | */ 210 | Line.prototype.clear = function () { 211 | 212 | this.text = ""; 213 | this.graphicProperties = {}; 214 | this.render(); 215 | 216 | }; 217 | 218 | Line.prototype.remove = function () { 219 | 220 | if (this._lineElement.parentNode) { 221 | this._lineElement.parentNode.removeChild(this._lineElement); 222 | } 223 | 224 | }; -------------------------------------------------------------------------------- /src/client/js/output/const.js: -------------------------------------------------------------------------------- 1 | export const COLOR_8BIT = { 2 | "0": "#000000", "1": "#800000", "2": "#008000", "3": "#808000", "4": "#000080", 3 | "5": "#800080", "6": "#008080", "7": "#c0c0c0", "8": "#808080", "9": "#ff0000", 4 | "10": "#00ff00", "11": "#ffff00", "12": "#0000ff", "13": "#ff00ff", "14": "#00ffff", 5 | "15": "#ffffff", "16": "#000000", "17": "#00005f", "18": "#000087", "19": "#0000af", 6 | "20": "#0000df", "21": "#0000ff", "22": "#005f00", "23": "#005f5f", "24": "#005f87", 7 | "25": "#005faf", "26": "#005fdf", "27": "#005fff", "28": "#008700", "29": "#00875f", 8 | "30": "#008787", "31": "#0087af", "32": "#0087df", "33": "#0087ff", "34": "#00af00", 9 | "35": "#00af5f", "36": "#00af87", "37": "#00afaf", "38": "#00afdf", "39": "#00afff", 10 | "40": "#00df00", "41": "#00df5f", "42": "#00df87", "43": "#00dfaf", "44": "#00dfdf", 11 | "45": "#00dfff", "46": "#00ff00", "47": "#00ff5f", "48": "#00ff87", "49": "#00ffaf", 12 | "50": "#00ffdf", "51": "#00ffff", "52": "#5f0000", "53": "#5f005f", "54": "#5f0087", 13 | "55": "#5f00af", "56": "#5f00df", "57": "#5f00ff", "58": "#5f5f00", "59": "#5f5f5f", 14 | "60": "#5f5f87", "61": "#5f5faf", "62": "#5f5fdf", "63": "#5f5fff", "64": "#5f8700", 15 | "65": "#5f875f", "66": "#5f8787", "67": "#5f87af", "68": "#5f87df", "69": "#5f87ff", 16 | "70": "#5faf00", "71": "#5faf5f", "72": "#5faf87", "73": "#5fafaf", "74": "#5fafdf", 17 | "75": "#5fafff", "76": "#5fdf00", "77": "#5fdf5f", "78": "#5fdf87", "79": "#5fdfaf", 18 | "80": "#5fdfdf", "81": "#5fdfff", "82": "#5fff00", "83": "#5fff5f", "84": "#5fff87", 19 | "85": "#5fffaf", "86": "#5fffdf", "87": "#5fffff", "88": "#870000", "89": "#87005f", 20 | "90": "#870087", "91": "#8700af", "92": "#8700df", "93": "#8700ff", "94": "#875f00", 21 | "95": "#875f5f", "96": "#875f87", "97": "#875faf", "98": "#875fdf", "99": "#875fff", 22 | "100": "#878700", "101": "#87875f", "102": "#878787", "103": "#8787af", "104": "#8787df", 23 | "105": "#8787ff", "106": "#87af00", "107": "#87af5f", "108": "#87af87", "109": "#87afaf", 24 | "110": "#87afdf", "111": "#87afff", "112": "#87df00", "113": "#87df5f", "114": "#87df87", 25 | "115": "#87dfaf", "116": "#87dfdf", "117": "#87dfff", "118": "#87ff00", "119": "#87ff5f", 26 | "120": "#87ff87", "121": "#87ffaf", "122": "#87ffdf", "123": "#87ffff", "124": "#af0000", 27 | "125": "#af005f", "126": "#af0087", "127": "#af00af", "128": "#af00df", "129": "#af00ff", 28 | "130": "#af5f00", "131": "#af5f5f", "132": "#af5f87", "133": "#af5faf", "134": "#af5fdf", 29 | "135": "#af5fff", "136": "#af8700", "137": "#af875f", "138": "#af8787", "139": "#af87af", 30 | "140": "#af87df", "141": "#af87ff", "142": "#afaf00", "143": "#afaf5f", "144": "#afaf87", 31 | "145": "#afafaf", "146": "#afafdf", "147": "#afafff", "148": "#afdf00", "149": "#afdf5f", 32 | "150": "#afdf87", "151": "#afdfaf", "152": "#afdfdf", "153": "#afdfff", "154": "#afff00", 33 | "155": "#afff5f", "156": "#afff87", "157": "#afffaf", "158": "#afffdf", "159": "#afffff", 34 | "160": "#df0000", "161": "#df005f", "162": "#df0087", "163": "#df00af", "164": "#df00df", 35 | "165": "#df00ff", "166": "#df5f00", "167": "#df5f5f", "168": "#df5f87", "169": "#df5faf", 36 | "170": "#df5fdf", "171": "#df5fff", "172": "#df8700", "173": "#df875f", "174": "#df8787", 37 | "175": "#df87af", "176": "#df87df", "177": "#df87ff", "178": "#dfaf00", "179": "#dfaf5f", 38 | "180": "#dfaf87", "181": "#dfafaf", "182": "#dfafdf", "183": "#dfafff", "184": "#dfdf00", 39 | "185": "#dfdf5f", "186": "#dfdf87", "187": "#dfdfaf", "188": "#dfdfdf", "189": "#dfdfff", 40 | "190": "#dfff00", "191": "#dfff5f", "192": "#dfff87", "193": "#dfffaf", "194": "#dfffdf", 41 | "195": "#dfffff", "196": "#ff0000", "197": "#ff005f", "198": "#ff0087", "199": "#ff00af", 42 | "200": "#ff00df", "201": "#ff00ff", "202": "#ff5f00", "203": "#ff5f5f", "204": "#ff5f87", 43 | "205": "#ff5faf", "206": "#ff5fdf", "207": "#ff5fff", "208": "#ff8700", "209": "#ff875f", 44 | "210": "#ff8787", "211": "#ff87af", "212": "#ff87df", "213": "#ff87ff", "214": "#ffaf00", 45 | "215": "#ffaf5f", "216": "#ffaf87", "217": "#ffafaf", "218": "#ffafdf", "219": "#ffafff", 46 | "220": "#ffdf00", "221": "#ffdf5f", "222": "#ffdf87", "223": "#ffdfaf", "224": "#ffdfdf", 47 | "225": "#ffdfff", "226": "#ffff00", "227": "#ffff5f", "228": "#ffff87", "229": "#ffffaf", 48 | "230": "#ffffdf", "231": "#ffffff", "232": "#080808", "233": "#121212", "234": "#1c1c1c", 49 | "235": "#262626", "236": "#303030", "237": "#3a3a3a", "238": "#444444", "239": "#4e4e4e", 50 | "240": "#585858", "241": "#606060", "242": "#666666", "243": "#767676", "244": "#808080", 51 | "245": "#8a8a8a", "246": "#949494", "247": "#9e9e9e", "248": "#a8a8a8", "249": "#b2b2b2", 52 | "250": "#bcbcbc", "251": "#c6c6c6", "252": "#d0d0d0", "253": "#dadada", "254": "#e4e4e4", 53 | "255": "#eeeeee" 54 | }; -------------------------------------------------------------------------------- /src/client/js/output/esc.js: -------------------------------------------------------------------------------- 1 | import * as output from "./index"; 2 | import { COLOR_8BIT } from "./const"; 3 | import * as server from "../server"; 4 | import * as caret from "../input/caret"; 5 | 6 | let cursorHome, 7 | savedCursorPosition = [], 8 | savedGraphicProperties = {}, 9 | temp; 10 | 11 | /** 12 | * DO NOT use output.print function inside: it may bring unexpected results as print function uses 13 | * printing stack. 14 | * The key is the sequence of symbols. There are some special ones. Examples: 15 | * "\nabc" Matches a newline and "abc" right after a newline. 16 | * "\n{[abc]+}" Regular expressions are covered by {} symbols. Those symbols must not be in regex. 17 | * "\n{![abc]+}" Regex-es can be mandatory, when the first character "!" is put (not a regex part). 18 | * Mandatory regex-es will block any output until the regex matches. 19 | */ 20 | let esc = { 21 | "\u000C": () => { 22 | output.clear(); 23 | }, 24 | "\n": () => { 25 | output.scrollDisplay(1); 26 | }, 27 | "\r": () => { 28 | output.setCursorX(1); 29 | }, 30 | // tab control 31 | "\t": () => { 32 | let x = output.getCursorX(), 33 | tabs = output.getTabs(); 34 | for (let i = 0; i < tabs.length; i++) { 35 | if (x < tabs[i]) { 36 | output.setCursorX(tabs[i]); 37 | return; 38 | } 39 | } 40 | }, 41 | "\x1bH": () => { 42 | output.setTabAt(output.getCursorX()); 43 | }, 44 | "\x1b[g": () => { 45 | output.clearTab(output.getCursorX()); 46 | }, 47 | "\x1b[3g": () => { 48 | output.clearTab(); 49 | }, 50 | // scrolling 51 | "\x1b[r": () => { 52 | output.disableScrolling(); 53 | }, 54 | "\x1b[{\\d*}{;?}{\\d*}r": (args) => { 55 | let start = args[0] || 1, 56 | end = args[2] || output.HEIGHT; 57 | if (!args[1]) 58 | start = args[0] || args[2]; 59 | if (!start) { 60 | output.disableScrolling(); 61 | return; 62 | } 63 | output.enableScrolling(start, end); 64 | }, 65 | "\x1bD": () => { 66 | output.scrollDisplay(1); 67 | }, 68 | "\x1bM": () => { 69 | output.scrollDisplay(-1); 70 | }, 71 | // status 72 | "\x1b[c": () => { 73 | server.send(`i`, `\x1b10c`); 74 | }, 75 | "\x1b[5n": () => { 76 | server.send(`i`, `\x1b0n`); 77 | }, 78 | "\x1b[6n": () => { 79 | server.send(`i`, `\x1b[${ output.getCursorY() };${ output.getCursorY() }n`); 80 | }, 81 | "\x1bc": () => { 82 | // Reset terminal settings to default. Caché TERM does not reset settings, indeed. 83 | output.LINE_WRAP_ENABLED = true; 84 | }, 85 | "\x1b[7h": () => { 86 | output.LINE_WRAP_ENABLED = true; 87 | }, 88 | "\x1b[7l": () => { 89 | output.LINE_WRAP_ENABLED = false; 90 | }, 91 | "\x1b[?25h": () => { 92 | caret.hide(); 93 | }, 94 | "\x1b[?25l": () => { 95 | caret.update(); 96 | }, 97 | // font control 98 | "\x1b(": () => { 99 | // set default font 100 | }, 101 | "\x1b)": () => { 102 | // set alternate font 103 | }, 104 | // printing 105 | "\x1b[{\\d*}i": () => { 106 | window.print(); 107 | }, 108 | // cursor control 109 | "\x1b[{\\d*}{;?}{\\d*}H": cursorHome = (args) => { 110 | if (args[0] || args[2]) { 111 | if (args[0]) 112 | output.setCursorY(+args[0]); 113 | if (args[2]) 114 | output.setCursorX(+args[2]); 115 | } else { 116 | output.setCursorX(1); 117 | output.setCursorY(1); 118 | } 119 | }, 120 | "\x1b[{\\d*}{;?}{\\d*}f": cursorHome, 121 | "\x1b[{\\d*}A": (args) => { 122 | output.setCursorY(Math.max(1, output.getCursorY() - (+args[0] || 1))); 123 | }, 124 | "\x1b[{\\d*}B": (args) => { 125 | output.setCursorY(Math.min(output.HEIGHT, output.getCursorY() + (+args[0] || 1))); 126 | }, 127 | "\x1b[{\\d*}C": (args) => { 128 | output.setCursorX(Math.min(output.WIDTH, output.getCursorX() + (+args[0] || 1))); 129 | }, 130 | "\x1b[{\\d*}D": (args) => { 131 | output.setCursorX(Math.max(1, output.getCursorX() - (+args[0] || 1))); 132 | }, 133 | "\x1b[{\\d*}G": (args) => { 134 | output.setCursorX(+args[0]); 135 | }, 136 | "\x1b[s": () => { 137 | savedCursorPosition = [output.getCursorX(), output.getCursorY()]; 138 | }, 139 | "\x1b[u": () => { 140 | if (!savedCursorPosition.length) 141 | return; 142 | output.setCursorX(savedCursorPosition[0]); 143 | output.setCursorY(savedCursorPosition[1]); 144 | }, 145 | "\x1b7": () => { 146 | savedCursorPosition = [output.getCursorX(), output.getCursorY()]; 147 | savedGraphicProperties = JSON.parse(JSON.stringify(output.GRAPHIC_PROPERTIES)); 148 | }, 149 | "\x1b8": () => { 150 | if (!savedCursorPosition.length) 151 | return; 152 | output.setCursorX(savedCursorPosition[0]); 153 | output.setCursorY(savedCursorPosition[1]); 154 | output.GRAPHIC_PROPERTIES = JSON.parse(JSON.stringify(savedGraphicProperties)); 155 | }, 156 | // erasing text 157 | "\x1b[K": temp = () => { 158 | let pos = output.getCursorX(), 159 | gp = output.GRAPHIC_PROPERTIES; 160 | output.resetGraphicProperties(); 161 | output.getCurrentLine().print(new Array(output.WIDTH - pos + 2).join(" "), pos - 1); 162 | output.GRAPHIC_PROPERTIES = gp; 163 | }, 164 | "\x1b[0K": temp, 165 | "\x1b[1K": () => { 166 | let pos = output.getCursorX(), 167 | gp = output.GRAPHIC_PROPERTIES; 168 | output.resetGraphicProperties(); 169 | output.getCurrentLine().print(new Array(pos + 1).join(" "), 0); 170 | output.GRAPHIC_PROPERTIES = gp; 171 | }, 172 | "\x1b[2K": () => { 173 | output.getCurrentLine().clear(); 174 | }, 175 | "\x1b[J": temp = () => { 176 | let y = output.getCursorY() + 1; 177 | esc["\x1b[K"](); 178 | for (; y < output.HEIGHT + 1; y++) { 179 | output.getLineByCursorY(y).clear(); 180 | } 181 | }, 182 | "\x1b[0J": temp, 183 | "\x1b[1J": () => { 184 | let y = output.getCursorY() - 1; 185 | esc["\x1b[1K"](); 186 | for (; y > 0; y--) { 187 | output.getLineByCursorY(y).clear(); 188 | } 189 | }, 190 | "\x1b[2J": () => { 191 | for (let y = 1; y < output.HEIGHT + 1; y++) { 192 | output.getLineByCursorY(y).clear(); 193 | } 194 | output.setCursorX(1); // Caché TERM does not set cursor to it's home position. 195 | output.setCursorY(1); // But standard requires this. 196 | }, 197 | // 198 | "\x1b[{\\d*};\"{[^\"]}\"p": (args) => { // define key 199 | console.log("todo: implement key assignment", args); 200 | }, 201 | "\x1b[{\\d*(?:;\\d*)*}m": (args) => { 202 | let indices = args[0].split(`;`); 203 | for (let i = 0; i < indices.length; i++) { 204 | if (indices[i] === "0" || indices[i] === "") { 205 | output.resetGraphicProperties(); 206 | continue; 207 | } 208 | if (indices[i] === "22") { 209 | output.clearGraphicProperty(1); 210 | output.clearGraphicProperty(2); 211 | continue; 212 | } 213 | if ((indices[i] === "38" || indices[i] === "48") && indices[i + 1] === "5") { 214 | output.setGraphicProperty(+indices[i], { 215 | class: `m${indices[i]}`, 216 | style: `${indices[i] === "48" ? "background-" : ""}color:${ COLOR_8BIT[indices[i + 2]] }` 217 | }); 218 | i += 2; 219 | continue; 220 | } 221 | output.setGraphicProperty(indices[i], { 222 | class: `m${indices[i]}` 223 | }); 224 | } 225 | }, 226 | "\x1b[({[\\w\\-]+})m": (args) => { // print element of class 227 | let cls = args[0]; 228 | if (!cls) 229 | return; 230 | output.setGraphicProperty(9, { class: cls }); 231 | }, 232 | "\x1b!URL={[^\\x20]*} ({[^\\)]+})": ([url = "", text = ""]) => { 233 | output.setGraphicProperty(9, { 234 | tag: "a", 235 | attrs: { 236 | href: url, 237 | target: url.indexOf("javascript:") === 0 ? "" : "_blank" 238 | } 239 | }); 240 | output.immediatePlainPrint(text); 241 | output.clearGraphicProperty(9); 242 | }, 243 | "\x1b!{![^]*(?=<\\/HTML>)}": ([ html ]) => { 244 | let line = output.getCurrentLine(); 245 | line.setHTML(html); 246 | let nextIndex = line.INDEX + Math.ceil(line.getHeight() / output.SYMBOL_HEIGHT); 247 | output.getLineByIndex(nextIndex); // ensure that line exists 248 | output.setCursorYToLineIndex(nextIndex); // jump to new index 249 | output.setCursorX(1); 250 | } 251 | }; 252 | 253 | export default esc; -------------------------------------------------------------------------------- /src/client/js/output/escStateMachine.js: -------------------------------------------------------------------------------- 1 | import esc from "./esc"; 2 | 3 | let stateTree = {}; 4 | 5 | function registerSequence (seq = "", f) { 6 | 7 | let seqPos = 0, 8 | pos, r, tree = stateTree; 9 | 10 | while (seqPos < seq.length) { 11 | if (seq[seqPos] === "{" && (pos = seq.indexOf("}", seqPos)) !== -1) { 12 | try { 13 | r = new RegExp(seq.substring(seqPos + 1, pos)); 14 | } catch (e) { console.error(`Malformed RegExp in "${ seq }" pattern.`, e); continue; } 15 | seqPos = pos + 1; 16 | tree = (tree[""] = tree[""] ? tree[""] : {})[r.source] = 17 | seqPos < seq.length ? (tree[""][r.source] ? tree[""][r.source] : {}) : f; 18 | continue; 19 | } 20 | tree = tree[seq[seqPos]] = 21 | seqPos + 1 < seq.length ? (tree[seq[seqPos]] ? tree[seq[seqPos]] : {}) : f; 22 | seqPos++; 23 | } 24 | 25 | } 26 | 27 | for (let seq in esc) { 28 | registerSequence(seq, esc[seq]); 29 | } 30 | 31 | export const ESC_CHARS_MASK = /[\x00-\x1F]/; 32 | 33 | function getMatched (o, str, args = []) { 34 | // console.log(`Diving with ${str} (${ str.length })`); 35 | if (!str) 36 | return 0; 37 | let a, m = -1, n, l; 38 | if (o[str[0]]) { 39 | if (typeof o[str[0]] === "function") { 40 | o[str[0]](args); 41 | // console.log(`Function found`); 42 | return 1; 43 | } 44 | m = getMatched(o[str[0]], str.substr(1), args); 45 | if (m > 0) { 46 | // console.log(`Sub-obj found`); 47 | return 1 + m; 48 | } 49 | } 50 | for (let p in o[""]) { 51 | let regEx = p[0] === "!" ? p.slice(1) : p; 52 | if (a = str.match(`^${ regEx }`)) { 53 | n = getMatched(o[""][p], str.substr(l = a.join().length), args.concat(a)); 54 | if (n > 0) 55 | return n + l; 56 | m = n > m ? n : m; 57 | } else if (p[0] === "!") { // mandatory reg exp not matched 58 | m = 0; 59 | } 60 | } 61 | return m; 62 | } 63 | 64 | /** 65 | * This function takes the string which begins from any registered escape sequence. 66 | * @see esc.js 67 | * It tries to parse and execute escape sequences that were found. 68 | * If escape sequence matches, the function processes it and returns the string without the 69 | * sequence. If no escape sequence matches it returns the input string without changes. 70 | * @returns {string} - Returns THE SAME STRING if more characters is needed in order to process 71 | * sequence. 72 | */ 73 | export function applyEscapeSequence (string = "") { 74 | let l = getMatched(stateTree, string); 75 | // console.log(`Input: ${ string }, getMatched: ${ l }`); 76 | return l === -1 ? string.substr(1) // unknown escape sequence, just remove esc character 77 | : l === 0 ? string // need more characters, wait 78 | : string.substr(l); // rest of the string 79 | } 80 | 81 | // todo: test and delete 82 | window.applyEscapeSequence = applyEscapeSequence; -------------------------------------------------------------------------------- /src/client/js/parser/_build.js: -------------------------------------------------------------------------------- 1 | // THIS MODULE IS INVOKED AT THE BUILD TIME. ANY ERRORS ARE REPORTED DURING THE GULP BUILD TASK \\ 2 | 3 | import "./grammar"; 4 | import { getAutomaton as ga } from "./pushdownAutomaton"; 5 | 6 | /** 7 | * Returns automaton. Evaluated at compile time. 8 | * @returns {[[[*, *, *]]]} - [match, next, stack] 9 | */ 10 | export function getAutomaton () { 11 | return ga(); 12 | } -------------------------------------------------------------------------------- /src/client/js/server/handlers.js: -------------------------------------------------------------------------------- 1 | import * as output from "../output"; 2 | import * as input from "../input"; 3 | import * as server from "./index"; 4 | import * as terminal from "../index"; 5 | import * as locale from "../localization"; 6 | import * as config from "../config"; 7 | import * as analytics from "../analytics"; 8 | 9 | export function init (data = {}) { 10 | if (config.get("initMessage") && !data["cleanStart"]) { 11 | output.printLine(`CWTv${ terminal.VERSION } ${ data["system"] }:\x1b[(keyword)m${ 12 | data["username"] }\x1b[0m${ data["name"] ? ` (${ data["name"] })` : `` }`); 13 | if (data["firstLaunch"]) 14 | output.printLine(locale.get(`firstLaunchMessage`)); 15 | } 16 | if (data["cleanStart"]) config.setTemp("updateCheck", "false"); 17 | analytics.collect(data); 18 | config.set(`serverName`, data["name"], true); 19 | document.title = `${ data["name"] || data["system"] } - WebTerminal`; 20 | terminal.authDone(); 21 | } 22 | 23 | export function prompt (namespace) { 24 | terminal.NAMESPACE = namespace; 25 | input.prompt(`${ namespace } > `, {}, (str) => { 26 | server.send("Execute", str); 27 | }); 28 | } 29 | 30 | export function promptCallback (data) { 31 | terminal.promptCallback(data); 32 | } 33 | 34 | function cleanCWTLabel (string) { 35 | let s = string.replace(/(\w+(?:\+[0-9]+)?\^(?:\w\.?)+)/, "\x1b[(special)m$1\x1b[0m") 36 | .replace(/^(<.*>)/, `\x1b[31m$1\x1b[0m`), 37 | ss = s.replace(/z\w+\+[0-9]+\^WebTerminal\.\w+\.[0-9]+/, ""); 38 | return { 39 | string: ss, 40 | internal: ss.length !== s.length 41 | }; 42 | } 43 | 44 | export function execError (message = "") { 45 | let textToPrint = [""]; 46 | if (typeof message === "object") { 47 | let label = cleanCWTLabel(message["zerror"] || "?"); 48 | textToPrint.push(label.string); 49 | if (!label.internal) { 50 | let source = message.source.split(/\n/g); 51 | for (let line = 0; line < source.length; line++) { 52 | let e = line === message.line; 53 | textToPrint.push( 54 | `\x1b[${ e ? "(wrong)" : "(special)" }m${ e ? "╠" : "║" }\x1b[0m` 55 | + (e ? "\x1b[(wrong)m" : "") 56 | + source[line] 57 | + (e ? "\x1b[0m" : "") 58 | ); 59 | } 60 | } 61 | } else { 62 | textToPrint.push(cleanCWTLabel(message).string); 63 | } 64 | output.printAsync(textToPrint.join("\r\n") + "\r\n"); 65 | } 66 | 67 | export function error (message = "") { 68 | output.print(`\x1b[31m${ locale.parse(message) }\x1b[0m`); 69 | } 70 | 71 | export function readString (data = {}) { 72 | input.prompt("", data, (str) => { 73 | server.send("i", str); 74 | }, false); 75 | } 76 | 77 | export function readChar (data = {}) { 78 | input.getKey(data, (code) => { 79 | server.send("i", code); 80 | }); 81 | } 82 | 83 | /** 84 | * Output data. 85 | * @param {string} text 86 | */ 87 | export function o (text = "") { 88 | output.print(text); 89 | } 90 | 91 | export function oLocalized (text = "") { 92 | output.print(locale.parse(text)); 93 | } -------------------------------------------------------------------------------- /src/client/js/server/index.js: -------------------------------------------------------------------------------- 1 | import { printLine } from "../output"; 2 | import { get as localize } from "../localization"; 3 | import * as handlers from "./handlers"; 4 | 5 | /* 6 | * WebSocket message body (parsed): 7 | * { 8 | * h: "HandlerName", 9 | * d: "data" 10 | * } 11 | * OR 12 | * "o" _ "" => is transformed to => { h: "o", d: "" } 13 | */ 14 | 15 | const CACHE_CLASS_NAME = `WebTerminal.Engine.cls`; 16 | const RECONNECT_IN = 10000; // ms 17 | 18 | let CONNECTED = false; 19 | 20 | let ws, 21 | stack = [], 22 | reconnectTimeout = 0, 23 | firstMessage, 24 | nextCallbackId = 1, 25 | callbacks = {}, 26 | firstConnectParameters = {}; 27 | 28 | export function connect (params) { 29 | clearTimeout(reconnectTimeout); 30 | reconnectTimeout = 0; 31 | ws = getNewWs(); 32 | if (params) 33 | firstConnectParameters = params; 34 | ws.addEventListener(`open`, onOpen); 35 | ws.addEventListener(`close`, onClose); 36 | ws.addEventListener(`error`, onError); 37 | ws.addEventListener(`message`, (m) => { 38 | if (m.data[0] === "o") { // Enables 2013.2 support (no JSON correct escaping) 39 | onMessage({ h: "o", d: m.data.slice(1) }); 40 | return; 41 | } 42 | let d; 43 | try { 44 | d = JSON.parse(m.data); 45 | } catch (e) { 46 | printLine(localize(`serParseErr`, m.data)); 47 | return; 48 | } 49 | onMessage(d); 50 | }); 51 | } 52 | 53 | /** 54 | * Connect to the server or return alive connection. 55 | */ 56 | function getNewWs () { 57 | return new WebSocket(`${ (location.protocol === "https:" ? "wss:" : "ws:") 58 | }//${ location.hostname }:${ location.port || 59 | (location.protocol === "https:" ? "443" : "80") 60 | }${ location.pathname.includes('/terminal') ? location.pathname.replace(/\/terminal.*/, "/terminalsocket") : "/terminalsocket" }/${ encodeURIComponent(CACHE_CLASS_NAME) }`); 61 | } 62 | 63 | function onOpen () { 64 | CONNECTED = true; 65 | send("Auth", firstConnectParameters); 66 | freeStack(); 67 | } 68 | 69 | function onError (e) { 70 | printLine(localize(`wsErr`, e.data || e)); 71 | } 72 | 73 | function reconnect () { 74 | printLine(localize(`plRefPageSes`)); // todo: restore session [https://community.intersystems.com/post/it-possible-not-terminate-jobbed-process-when-parent-process-terminates] 75 | stack.unshift(firstMessage); 76 | connect(); 77 | } 78 | 79 | function onClose (e) { 80 | CONNECTED = false; 81 | if (e.code !== 1000 && e.code !== 1005) { 82 | printLine(`\r\n${ localize(`wsConnLost`, e.code, e.reason ? " " + e.reason : "") }`); 83 | printLine(localize(`reConn`, RECONNECT_IN / 1000)); 84 | reconnectTimeout = setTimeout(reconnect, RECONNECT_IN); 85 | } else { 86 | printLine(localize(`seeYou`)); 87 | } 88 | } 89 | 90 | function onMessage (data = {}) { 91 | if (data._cb) { 92 | if (!callbacks[data._cb]) { 93 | printLine(`\r\n` + localize(`eInt`, `E.server.index.3 (${ data._cb })`)); 94 | return; 95 | } 96 | callbacks[data._cb](data.d); 97 | delete callbacks[data._cb]; 98 | } else if (data.h) { 99 | if (typeof handlers[data.h] === "function") { 100 | handlers[data.h](data.d); 101 | } else { 102 | printLine(`\r\n` + localize(`eInt`, `E.server.index.1 (${ data.h })`)); 103 | } 104 | } else { 105 | printLine(`\r\n` + localize(`eInt`, `E.server.index.2 (${ JSON.stringify(data) })`)); 106 | } 107 | } 108 | 109 | function freeStack () { 110 | if (!CONNECTED) 111 | return; 112 | stack = stack.filter((m) => { 113 | try { 114 | ws.send(JSON.stringify(m)); 115 | } catch (e) { 116 | if (!reconnectTimeout) 117 | reconnectTimeout = setTimeout(reconnect, RECONNECT_IN); 118 | return true; 119 | } 120 | return false; 121 | }); 122 | } 123 | 124 | /** 125 | * Send message to a server. 126 | * @param {string} handler - Handler name. 127 | * @param {*} [data] 128 | * @param {function} [callback] 129 | */ 130 | export function send (handler, data, callback) { 131 | let message = { 132 | d: data || null, 133 | h: handler 134 | }; 135 | if (typeof callback === "function") { 136 | message._cb = nextCallbackId; 137 | callbacks[nextCallbackId++] = callback; 138 | } 139 | if (!firstMessage) 140 | firstMessage = message; 141 | stack.push(message); 142 | freeStack(); 143 | } -------------------------------------------------------------------------------- /src/client/js/settings.js: -------------------------------------------------------------------------------- 1 | import * as storage from "./storage"; 2 | 3 | const STORAGE_NAME = "terminal-settings"; 4 | 5 | /** 6 | * @readonly 7 | * @type {{SHOW_PROGRESS_INDICATOR: boolean, HIGHLIGHT_INPUT: boolean, AUTOCOMPLETE: boolean}} 8 | */ 9 | export let OPTIONS = JSON.parse(storage.get(STORAGE_NAME)) || { 10 | SHOW_PROGRESS_INDICATOR: true, 11 | HIGHLIGHT_INPUT: true, 12 | AUTOCOMPLETE: true 13 | }; 14 | 15 | /** 16 | * Set the option. 17 | * @param {string} option 18 | * @param {*} value 19 | */ 20 | export function set (option, value) { 21 | OPTIONS[option] = value; 22 | storage.set(STORAGE_NAME, JSON.stringify(OPTIONS)); 23 | } -------------------------------------------------------------------------------- /src/client/js/storage.js: -------------------------------------------------------------------------------- 1 | import { printLine } from "./output"; 2 | import * as locale from "./localization"; 3 | 4 | if (typeof JSON === "undefined" || typeof localStorage === "undefined") { 5 | printLine(locale.get(`storageErr`)); 6 | } 7 | 8 | /** 9 | * Clears all stored data. 10 | */ 11 | export function clear () { 12 | localStorage.clear(); 13 | } 14 | 15 | /** 16 | * Sets the local storage key. 17 | * @param {string} key 18 | * @param {string} value 19 | */ 20 | export function set (key, value) { 21 | localStorage.setItem(key, value); 22 | } 23 | 24 | /** 25 | * Removes value of key from storage. 26 | * @param {string} key 27 | */ 28 | export function remove (key) { 29 | localStorage.removeItem(key); 30 | } 31 | 32 | /** 33 | * Gets local storage key value. 34 | * @param {string} key 35 | * @returns {string|null} 36 | */ 37 | export function get (key) { 38 | return localStorage.getItem(key); 39 | } -------------------------------------------------------------------------------- /src/client/js/tracing/index.js: -------------------------------------------------------------------------------- 1 | import * as server from "../server"; 2 | import * as output from "../output"; 3 | 4 | const TRACE_CHECK_INTERVAL = 1000; 5 | 6 | let tracing = [], 7 | timeout = 0; 8 | 9 | export function getList () { 10 | return tracing.map(e => "\x1b[(" + (e[0] === "^" ? "global" : "string") + ")m" + e 11 | + "\x1b[0m").join(", "); 12 | } 13 | 14 | export function start (v) { 15 | if (tracing.indexOf(v) !== -1) 16 | return; 17 | tracing.push(v); 18 | if (timeout === 0) 19 | timeout = setTimeout(checkTrace, TRACE_CHECK_INTERVAL); 20 | } 21 | 22 | export function stop (v) { 23 | if (!v) { 24 | tracing = []; 25 | if (timeout) clearTimeout(timeout); 26 | return; 27 | } 28 | tracing = tracing.filter(e => e !== v); 29 | if (tracing.length > 0) 30 | return; 31 | if (timeout) { 32 | clearTimeout(timeout); 33 | timeout = 0; 34 | } 35 | } 36 | 37 | function checkTrace () { 38 | if (tracing.length === 0) { 39 | timeout = 0; 40 | return; 41 | } 42 | server.send("TracingStatus", {}, (obj) => { 43 | if (obj["changes"]) { 44 | output.printAsync(obj["changes"]); 45 | } 46 | for (let p in obj["stop"]) { 47 | stop(p); 48 | } 49 | timeout = setTimeout(checkTrace, TRACE_CHECK_INTERVAL); 50 | }); 51 | } -------------------------------------------------------------------------------- /src/client/scss/graphic.scss: -------------------------------------------------------------------------------- 1 | @import "mixins"; 2 | 3 | a { 4 | color: #00ce00; 5 | } 6 | 7 | .terminal .output .line { 8 | 9 | .g.m1 { /* bright */ 10 | font-weight: 900; 11 | } 12 | 13 | .g.m2 { /* dim */ 14 | opacity: 0.7; 15 | } 16 | 17 | .g.m3 { 18 | font-style: italic; 19 | } 20 | 21 | .g.m4 { 22 | text-decoration: underline; 23 | } 24 | 25 | .g.m5 { 26 | @include animation(blink infinite 1s); 27 | } 28 | 29 | .g.m7 { 30 | @include filter(invert(100%)); 31 | } 32 | 33 | .g.m8 { 34 | opacity: 0; 35 | } 36 | 37 | //.g.m9 { // reserved for CWT 38 | // opacity: 0.5; 39 | //} 40 | 41 | .g.hint { 42 | opacity: .5; 43 | } 44 | 45 | .g.m30 { 46 | color: #000000; 47 | } 48 | 49 | .g.m31 { 50 | color: #ff0000; 51 | } 52 | 53 | .g.m32 { 54 | color: #008000; 55 | } 56 | 57 | .g.m33 { 58 | color: yellow; 59 | } 60 | 61 | .g.m34 { 62 | color: #0000ff; 63 | } 64 | 65 | .g.m35 { 66 | color: magenta; 67 | } 68 | 69 | .g.m36 { 70 | color: cyan; 71 | } 72 | 73 | .g.m37 { 74 | color: white; 75 | } 76 | 77 | .g.m40 { 78 | background-color: #000000; 79 | } 80 | 81 | .g.m41 { 82 | background-color: #ff0000; 83 | } 84 | 85 | .g.m42 { 86 | background-color: #008000; 87 | } 88 | 89 | .g.m43 { 90 | background-color: yellow; 91 | } 92 | 93 | .g.m44 { 94 | background-color: #0000ff; 95 | } 96 | 97 | .g.m45 { 98 | background-color: magenta; 99 | } 100 | 101 | .g.m46 { 102 | background-color: cyan; 103 | } 104 | 105 | .g.m47 { 106 | background-color: white; 107 | } 108 | 109 | .g.keyword { 110 | color: #4898ff; 111 | } 112 | 113 | .g.string { 114 | color: #00c700; 115 | } 116 | 117 | .g.constant { 118 | color: cyan; 119 | } 120 | 121 | .g.special { 122 | color: yellow; 123 | } 124 | 125 | .g.variable { 126 | color: lightsalmon; 127 | } 128 | 129 | .g.selected { 130 | color: white; 131 | background-color: royalblue; 132 | } 133 | 134 | .g.argument { 135 | color: #da7cff; 136 | } 137 | 138 | .g.error { 139 | 140 | display: inline-block; 141 | position: relative; 142 | 143 | &:after { 144 | content: ""; 145 | position: absolute; 146 | display: block; 147 | bottom: 0; 148 | left: 0; 149 | width: 100%; 150 | height: 0; 151 | border-top: 1px dashed red; 152 | } 153 | 154 | } 155 | 156 | .g.wrong { 157 | color: red; 158 | } 159 | 160 | .g.global { 161 | color: #ef6913; 162 | } 163 | 164 | .g.classname { 165 | color: cyan; 166 | } 167 | 168 | } 169 | 170 | @include keyframes(blink) { 171 | 0% { @include filter(invert(0%)); } 172 | 50% { @include filter(invert(100%)); } 173 | 100% { @include filter(invert(0%)); } 174 | } 175 | 176 | @include keyframes(pulsate) { 177 | 0% { 178 | transform: scale(.1); 179 | -o-transform: scale(.1); 180 | -ms-transform: scale(.1); 181 | -moz-transform: scale(.1); 182 | -webkit-transform: scale(.1); 183 | opacity: 0; 184 | } 185 | 50% { opacity: 1; } 186 | 100% { 187 | transform: scale(.5); 188 | -o-transform: scale(.5); 189 | -ms-transform: scale(.5); 190 | -moz-transform: scale(.5); 191 | -webkit-transform: scale(.5); 192 | opacity: 0; 193 | } 194 | } -------------------------------------------------------------------------------- /src/client/scss/hintBox.scss: -------------------------------------------------------------------------------- 1 | @import "mixins"; 2 | 3 | .terminal .hintBox { 4 | 5 | position: absolute; 6 | left: 0; 7 | top: 0; 8 | width: 300px; 9 | max-width: 75%; 10 | height: 0; 11 | overflow: visible; 12 | z-index: 100; 13 | 14 | > div { 15 | 16 | position: absolute; 17 | @include transition(all .3s ease); 18 | color: gray; 19 | border-radius: 10px; 20 | 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/client/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import "terminal"; 2 | @import "graphic"; 3 | @import "hintBox"; -------------------------------------------------------------------------------- /src/client/scss/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin user-select ($value: none) { 2 | -webkit-user-select: $value; 3 | -moz-user-select: $value; 4 | -ms-user-select: $value; 5 | user-select: $value; 6 | } 7 | 8 | @mixin transition ($options...) { 9 | -webkit-transition: $options; 10 | -moz-transition: $options; 11 | -ms-transition: $options; 12 | -o-transition: $options; 13 | transition: $options; 14 | } 15 | 16 | @mixin filter ($options) { 17 | -khtml-filter: $options; 18 | -moz-filter: $options; 19 | -o-filter: $options; 20 | -ms-filter: $options; 21 | -webkit-filter: $options; 22 | filter: $options; 23 | } 24 | 25 | @mixin transform ($options) { 26 | -webkit-transform: $options; 27 | -moz-transform: $options; 28 | -ms-transform: $options; 29 | -o-transform: $options; 30 | transform: $options; 31 | } 32 | 33 | @mixin animation-delay ($options) { 34 | -webkit-animation-delay: $options; 35 | -moz-animation-delay: $options; 36 | -o-animation-delay: $options; 37 | animation-delay: $options; 38 | } 39 | 40 | @mixin keyframes ($animationName) { 41 | @-webkit-keyframes #{$animationName} { 42 | @content; 43 | } 44 | @-moz-keyframes #{$animationName} { 45 | @content; 46 | } 47 | @-ms-keyframes #{$animationName} { 48 | @content; 49 | } 50 | @-o-keyframes #{$animationName} { 51 | @content; 52 | } 53 | @keyframes #{$animationName} { 54 | @content; 55 | } 56 | } 57 | 58 | @mixin animation ($animate...) { 59 | $max: length($animate); 60 | $animations: ''; 61 | @for $i from 1 through $max { 62 | $animations: #{$animations + nth($animate, $i)}; 63 | @if $i < $max { 64 | $animations: #{$animations + ", "}; 65 | } 66 | } 67 | -webkit-animation: $animations; 68 | -moz-animation: $animations; 69 | -ms-animation: $animations; 70 | -o-animation: $animations; 71 | animation: $animations; 72 | } -------------------------------------------------------------------------------- /src/client/scss/terminal.scss: -------------------------------------------------------------------------------- 1 | @import "mixins"; 2 | 3 | html, body { 4 | position: relative; 5 | width: 100%; 6 | height: 100%; 7 | padding: 0; 8 | margin: 0; 9 | overflow: hidden; 10 | background: black; 11 | color: white; 12 | } 13 | 14 | .terminal { 15 | 16 | position: relative; 17 | width: 100%; 18 | height: 100%; 19 | overflow: hidden; 20 | font-family: FreeMono, monospace; 21 | font-size: 16px; 22 | background: inherit; 23 | -webkit-font-smoothing: subpixel-antialiased; 24 | -moz-osx-font-smoothing: auto; 25 | 26 | .output { 27 | 28 | position: absolute; 29 | white-space: pre; 30 | word-wrap: break-word; 31 | word-break: break-all; 32 | overflow-x: hidden; 33 | overflow-y: scroll; 34 | background: inherit; 35 | width: 100%; 36 | height: 100%; 37 | z-index: 0; 38 | 39 | .input { 40 | 41 | position: absolute; 42 | width: 100%; 43 | border: 0; 44 | text-decoration: none; 45 | outline: none; 46 | margin: 0; 47 | padding: 0; 48 | opacity: 0; 49 | font-family: inherit; 50 | font-size: inherit; 51 | z-index: 3; 52 | background: transparent; 53 | color: transparent; 54 | overflow: hidden; 55 | 56 | } 57 | 58 | .line { 59 | 60 | position: absolute; 61 | z-index: 1; 62 | min-width: 100%; 63 | background: inherit; 64 | 65 | .g { 66 | background: inherit; 67 | } 68 | 69 | &.html { 70 | z-index: 2; 71 | white-space: normal; 72 | height: auto !important; 73 | word-break: normal; 74 | overflow-x: auto; 75 | width: 100%; 76 | } 77 | 78 | } 79 | 80 | .caret { 81 | 82 | position: absolute; 83 | left: 0; 84 | top: 0; 85 | z-index: 2; 86 | @include animation(blink infinite 1s); 87 | 88 | &:after { 89 | position: relative; 90 | top: -1px; 91 | content: "_"; 92 | } 93 | 94 | } 95 | 96 | } 97 | 98 | > .indicator { 99 | position: fixed; 100 | right: 13px; 101 | top: 7px; 102 | border: 3px solid #fff; 103 | border-radius: 30px; 104 | height: 30px; 105 | margin: -15px 0 0 -15px; 106 | opacity: 0; 107 | width: 30px; 108 | @include animation(pulsate infinite 1s); 109 | } 110 | 111 | table { // Terminal may include custom HTML. This is a default style. 112 | border-collapse: collapse; 113 | border: 1px solid #666; 114 | th, td { 115 | padding: 2px; 116 | border: 1px solid #666; 117 | } 118 | td:nth-child(2n-1), th:nth-child(2n-1) { 119 | background: #222; 120 | } 121 | th { 122 | font-weight: bold; 123 | } 124 | } 125 | 126 | } -------------------------------------------------------------------------------- /src/client/scss/themes/cache.scss: -------------------------------------------------------------------------------- 1 | //! This file currently is not used in the project. 2 | 3 | html, body { 4 | color: black; 5 | background: white; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | .terminal .hint { 11 | text-shadow: 1px 1px 4px white; 12 | } 13 | 14 | .g.m31 { 15 | color: #c80000; 16 | } 17 | 18 | .g.m32 { 19 | color: #00c800; 20 | } 21 | 22 | .g.m33 { 23 | color: #c8c800; 24 | } 25 | 26 | .g.m34 { 27 | color: #0000c8; 28 | } 29 | 30 | .g.m35 { 31 | color: #c800c8; 32 | } 33 | 34 | .g.m36 { 35 | color: #00c8c8; 36 | } 37 | 38 | .g.m41 { 39 | background-color: #c80000; 40 | } 41 | 42 | .g.m42 { 43 | background-color: #00c800; 44 | } 45 | 46 | .g.m43 { 47 | background-color: #c8c800; 48 | } 49 | 50 | .g.m44 { 51 | background-color: #0000c8; 52 | } 53 | 54 | .g.m45 { 55 | background-color: #c800c8; 56 | } 57 | 58 | .g.m46 { 59 | background-color: #00c8c8; 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/cls/WebTerminal/Analytics.cls: -------------------------------------------------------------------------------- 1 | /// This class includes methods which collect WebTerminal's analytics such as error and installation reports. 2 | Class WebTerminal.Analytics 3 | { 4 | 5 | /// This method sends a report about installation status, including error message if any errors happened. 6 | ClassMethod ReportInstallStatus (status As %Status = 1, type As %String = "Install") As %Status 7 | { 8 | set req = ##class(%Net.HttpRequest).%New() 9 | set req.Server = "www.google-analytics.com" 10 | do req.EntityBody.Write("v=1&tid=&cid="_##class(%SYS.System).InstanceGUID() 11 | _"&ds=web&an=WebTerminal&av="_##class(WebTerminal.Installer).#VERSION 12 | _"&t=event&aiid="_$ZCONVERT($zv, "O", "URL")_"&ec="_$ZCONVERT(type, "O", "URL")_"&ea=" 13 | _$case($$$ISOK(status), 1: "Success", : "Failure")_"&el=" 14 | _$ZCONVERT($System.Status.GetErrorText(status), "O", "URL")) 15 | try { 16 | return req.Post("/collect") 17 | } catch e { 18 | write "Unable to send analytics to " _ req.Server _ ", skipping analytics collection." 19 | return $$$OK 20 | } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/cls/WebTerminal/Autocomplete.cls: -------------------------------------------------------------------------------- 1 | Class WebTerminal.Autocomplete Extends Common 2 | { 3 | 4 | /// Returns a comma-delimited string of globals names in the namespace, which begin from "beginning". 5 | ClassMethod GetGlobals(namespace As %String = "%SYS", beginning As %String = "") As %String 6 | { 7 | set result = "" 8 | set pattern = beginning _ "*" 9 | new $Namespace 10 | set $Namespace = namespace 11 | set rset = ##class(%ResultSet).%New("%SYS.GlobalQuery:NameSpaceList") 12 | do rset.Execute($Namespace, pattern, 1) 13 | while (rset.Next()) { 14 | set result = result _ $case(result = "", 1:"", :",") _ rset.GetData(1) 15 | } 16 | return result 17 | } 18 | 19 | /// Returns a comma-delimited string of class names in the namespace, which begin from "beginning". 20 | ClassMethod GetClass(namespace As %String = "%SYS", beginning As %String = "") As %String 21 | { 22 | new $Namespace 23 | set $Namespace = namespace 24 | set pattern = $REPLACE(beginning, "%", "!%") _ "%" 25 | &sql(select LIST(ID) into :ids from %Dictionary.CompiledClass where ID like :pattern ESCAPE '!' and deployed <> 2) 26 | return ids 27 | } 28 | 29 | /// Returns a comma-delimited string of public class members (accessible through ##class() construction) in the class of namespace. 30 | ClassMethod GetPublicClassMembers(namespace As %String = "%SYS", className As %String = "", beginning As %String = "") As %String 31 | { 32 | new $Namespace 33 | set $Namespace = namespace 34 | set pattern = $REPLACE(beginning, "%", "!%") _ "%" 35 | &sql(select LIST(Name) into :names from %Dictionary.CompiledMethod WHERE parent=:className AND ClassMethod=1 AND Name like :pattern ESCAPE '!') 36 | return names 37 | } 38 | 39 | /// Returns a comma-delimited string of class members in the class of namespace. 40 | ClassMethod GetClassMembers(namespace As %String = "%SYS", className As %String = "", beginning As %String = "", methodsOnly = "") As %String 41 | { 42 | new $Namespace 43 | set $Namespace = namespace 44 | if $EXTRACT(beginning, 1) = "#" { 45 | set ps = ..GetParameters(namespace, className, $EXTRACT(beginning, 2, $LENGTH(beginning))) 46 | return:(ps = "") ps 47 | return "#"_$REPLACE(ps, ",", ",#") 48 | } 49 | set pattern = $REPLACE(beginning, "%", "!%") _ "%" 50 | set props = "" 51 | &sql(select LIST(Name) into :methods from %Dictionary.CompiledMethod WHERE parent=:className AND Private = 0 AND Name like :pattern ESCAPE '!') 52 | if (methodsOnly = "") { 53 | &sql(select LIST(Name) into :props from %Dictionary.CompiledProperty WHERE parent=:className AND Name like :pattern ESCAPE '!') 54 | } 55 | return $case((methods '= "") && (props '= ""), 1: methods _ "," _ props, : methods _ props) 56 | } 57 | 58 | /// Returns a comma-delimited string of class members in the class of namespace. 59 | ClassMethod GetParameters(namespace As %String = "%SYS", className As %String = "", beginning As %String = "") As %String 60 | { 61 | new $Namespace 62 | set $Namespace = namespace 63 | set pattern = $REPLACE(beginning, "%", "!%") _ "%" 64 | &sql(select LIST(Name) into :names from %Dictionary.CompiledParameter WHERE parent=:className AND Name like :pattern ESCAPE '!') 65 | return names 66 | } 67 | 68 | /// Returns a comma-delimited string of routine names in the namespace, which begin from "beginning". 69 | ClassMethod GetRoutines(namespace As %String = "%SYS", beginning As %String = "") As %String 70 | { 71 | set result = "" 72 | set pattern = beginning _ "*.*" 73 | new $Namespace 74 | set $Namespace = namespace 75 | set rset = ##class(%ResultSet).%New("%Library.Routine:RoutineList") 76 | do rset.Execute(pattern, , , $Namespace) 77 | while (rset.Next()) { 78 | set result = result _ $case(result = "", 1:"", :",") _ $PIECE(rset.GetData(1), ".", 1, *-1) 79 | } 80 | return result 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/cls/WebTerminal/Common.cls: -------------------------------------------------------------------------------- 1 | Include %sySystem 2 | 3 | Class WebTerminal.Common 4 | { 5 | 6 | /// Interprocess communication cannot handle big messages at once, so they need to be split. 7 | Parameter ChunkSize = 45; 8 | 9 | /// Send the chunk of data to another process. The process need to receive the chunk with the 10 | /// appropriate function ReceiveChunk. Consider event length less than 44 characters long. 11 | ClassMethod SendChunk(pid As %Numeric, flag As %String, data As %String = "") As %Status 12 | { 13 | set pos = 1 14 | set len = $LENGTH(data) + 1 // send the last empty message if the data size = ChunkSize 15 | for { 16 | try { 17 | set st = $system.Event.Signal( 18 | pid, 19 | $LB(flag, $EXTRACT(data, pos, pos + ..#ChunkSize - 1)) 20 | ) 21 | } catch (e) { return $$$NOTOK } 22 | if (st '= 1) { return $$$NOTOK } 23 | set pos = pos + ..#ChunkSize 24 | if (pos > len) { quit } 25 | } 26 | return $$$OK 27 | } 28 | 29 | /// Receives the chunk of data from another process. Returns the $LISTBUILD string which contains 30 | /// flag at the first position and string at the second. This method also terminates the process 31 | /// if the parent process is gone. 32 | ClassMethod ReceiveChunk(timeout As %Numeric = -1, masterProcess = 0) As %String 33 | { 34 | set flag = "" 35 | set str = "" 36 | set status = -1 37 | for { 38 | set message = $system.Event.WaitMsg("", $Case(timeout = -1, 1: 1, :timeout)) 39 | set status = $LISTGET(message, 1) 40 | set data = $LISTGET(message, 2) 41 | if (status <= 0) { 42 | if ($ZPARENT '= 0) && ('$data(^$Job($ZPARENT))) { 43 | do $system.Process.Terminate($JOB, 0) 44 | return $LISTBUILD("e", $LISTBUILD("", "Parent process "_$JOB_" is gone"), -1) 45 | } 46 | if masterProcess && ($ZCHILD '= 0) && ('$data(^$Job($ZCHILD))) { 47 | return $LISTBUILD("e", $LISTBUILD("", "Child process "_$ZCHILD_" is gone"), -1) 48 | } 49 | } 50 | if (data = "") && (timeout = 0) quit 51 | if (status <= 0) { 52 | set:(timeout = 0) timeout = 1 53 | continue 54 | } 55 | set flag = $LISTGET(data, 1) 56 | set m = $LISTGET(data, 2) 57 | set str = str _ m 58 | if (timeout = 0) set timeout = 1 59 | quit:($LENGTH(m) '= ..#ChunkSize) 60 | } 61 | return $LISTBUILD(flag, str, status) 62 | } 63 | 64 | /// Returns the contents of the proxy object to the current device in JSON format.
65 | /// This method is called when a proxy object is used in conjunction with 66 | /// the %ZEN.Auxiliary.jsonProvider component.
67 | /// format is a flags string to control output formatting options.
68 | /// The following character option codes are supported:
69 | /// 1-9 : indent with this number of spaces (4 is the default with the 'i' format specifier)
70 | /// a - output null arrays/objects
71 | /// b - line break before opening { of objects
72 | /// c - output the Caché-specific "_class" and "_id" properties (if a child property is an instance of a concrete object class)
73 | /// e - output empty object properties
74 | /// i - indent with 4 spaces unless 't' or 1-9
75 | /// l - output empty lists
76 | /// n - newline (lf)
77 | /// o - output empty arrays/objects
78 | /// q - output numeric values unquoted even when they come from a non-numeric property
79 | /// s - use strict JSON output - NOTE: special care should be taken when sending data to a browser, as using this flag 80 | /// may expose you to cross site scripting (XSS) vulnerabilities if the data is sent inside <script> tags. Zen uses 81 | /// this technique extensively, so this flag should NOT be specified for jsonProviders in Zen pages.
82 | /// t - indent with tab character
83 | /// u - output pre-converted to UTF-8 instead of in native internal format
84 | /// w - Windows-style cr/lf newline
85 | ClassMethod GetJSONString(obj As %ZEN.proxyObject, format As %String = "aeos") As %String [ ProcedureBlock = 0 ] 86 | { 87 | set tOldIORedirected = ##class(%Device).ReDirectIO() 88 | set tOldMnemonic = ##class(%Device).GetMnemonicRoutine() 89 | set tOldIO = $io 90 | try { 91 | set str = "" 92 | use $io::("^" _ $ZNAME) 93 | do ##class(%Device).ReDirectIO(1) 94 | do ##class(%ZEN.Auxiliary.jsonProvider).%ObjectToJSON(obj,,,format) 95 | } catch ex { 96 | set str = "" 97 | } 98 | if (tOldMnemonic '= "") { 99 | use tOldIO::("^" _ tOldMnemonic) 100 | } else { 101 | use tOldIO 102 | } 103 | do ##class(%Device).ReDirectIO(tOldIORedirected) 104 | return str 105 | 106 | rchr(c) 107 | quit 108 | rstr(sz,to) 109 | quit 110 | wchr(s) 111 | do output($char(s)) 112 | quit 113 | wff() 114 | do output($char(12)) 115 | quit 116 | wnl() 117 | do output($char(13,10)) 118 | quit 119 | wstr(s) 120 | do output(s) 121 | quit 122 | wtab(s) 123 | do output($char(9)) 124 | quit 125 | output(s) 126 | set str = str _ s 127 | quit 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/cls/WebTerminal/Engine.cls: -------------------------------------------------------------------------------- 1 | /// version WebSocket client. 2 | /// This class represents a connected client via WebSocket. 3 | Class WebTerminal.Engine Extends (%CSP.WebSocket, Common, Trace, Autocomplete) 4 | { 5 | 6 | /// Timeout in minutes when connection key expires. 7 | Parameter WSKEYEXPIRES = 3600; 8 | 9 | /// How long to wait for authorization key when connection established 10 | Parameter AuthorizationTimeout = 5; 11 | 12 | Property CurrentNamespace As %String; 13 | 14 | Property InitialZName As %String; 15 | 16 | Property InitialZNamespace As %String; 17 | 18 | /// The process ID of the terminal core. 19 | Property corePID As %Numeric [ InitialExpression = 0 ]; 20 | 21 | /// The last known namespace in child process. 22 | Property childNamespace As %String; 23 | 24 | Property StartupRoutine As %String; 25 | 26 | /// Output flag 27 | Property echo As %Boolean [ InitialExpression = 1 ]; 28 | 29 | /// flag which enables output buffering 30 | Property bufferOutput As %Boolean [ InitialExpression = 0 ]; 31 | 32 | /// Used to buffer the output ("o" flag) when bufferOutput flag is set 33 | Property outputBuffer As %Stream.TmpCharacter [ InitialExpression = {##class(%Stream.TmpCharacter).%New()} ]; 34 | 35 | /// Output flag 36 | Property handler As %Boolean [ InitialExpression = 0, Private ]; 37 | 38 | Method GetMessage(timeout As %Integer = 86400) As %ZEN.proxyObject 39 | { 40 | #define err(%e, %s) if (%e '= $$$OK) { set obj = ##class(%ZEN.proxyObject).%New() set obj.error = %s return obj } 41 | set data = ..Read(, .st, timeout) 42 | return:((st = $$$CSPWebSocketTimeout) || (st = $$$CSPWebSocketClosed)) "" 43 | $$$err(st, "%wsReadErr: "_$System.Status.GetErrorText(st)) 44 | set st = ##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(data, , .obj, 1) 45 | $$$err(st, "%wsParseErr: "_$System.Status.GetErrorText(st)) 46 | return obj 47 | } 48 | 49 | /// Do not remove this method in future versions of WebTerminal, it is used by update. 50 | Method Send(handler As %String = "", data = "") As %Status 51 | { 52 | if (handler = "o") && (..bufferOutput = 1) { 53 | do ..outputBuffer.Write(data) 54 | return $$$OK 55 | } 56 | return:((handler = "o") && (..echo = 0)) $$$OK 57 | return:(handler = "o") ..Write("o"_data) // Enables 2013.2 support (no JSON) 58 | set obj = ##class(%ZEN.proxyObject).%New() 59 | set obj.h = handler 60 | if (..handler '= 0) { 61 | set obj."_cb" = ..handler 62 | } 63 | set obj.d = data 64 | return ..Write(..GetJSONString(obj)) 65 | } 66 | 67 | Method OnPreServer() As %Status 68 | { 69 | set ..InitialZName = $zname 70 | set ..InitialZNamespace = $znspace 71 | quit $$$OK 72 | } 73 | 74 | Method OnPostServer() As %Status 75 | { 76 | if (..corePID '= 0) { 77 | do ..SendChunk(..corePID, "e") 78 | } 79 | quit $$$OK 80 | } 81 | 82 | ClassMethod WriteToFile(filename As %String, data As %String) As %Status 83 | { 84 | set file=##class(%File).%New(filename) 85 | do file.Open("WSN") 86 | do file.WriteLine(data) 87 | do file.Close() 88 | } 89 | 90 | Method ExecuteSQL(query As %String = "") As %Status 91 | { 92 | set tStatement = ##class(%SQL.Statement).%New() 93 | set qStatus = tStatement.%Prepare(query) 94 | if qStatus'=1 { 95 | write $System.Status.DisplayError(qStatus) 96 | } else { 97 | set rset = tStatement.%Execute() 98 | do rset.%Display() 99 | } 100 | quit $$$OK 101 | } 102 | 103 | /// This method performs the authorization and login to WebTerminal. 104 | /// It returns a list with data (see Router.Auth method), which is used then to set up the 105 | /// initial values for the client. 106 | Method RequireAuthorization() As %List 107 | { 108 | set data = ..GetMessage(..#AuthorizationTimeout) 109 | return:(data = "") $LB("%wsReadErr") 110 | return:('$IsObject(data.d)) $LB($case(data.error = "", 1: "Unresolved WS message format", :data.error)) 111 | return:(data.d.key = "") $LB("Missing key") 112 | 113 | set authKey = data.d.key 114 | set key = $ORDER(^WebTerminal("AuthUser", "")) 115 | set list = "" 116 | while (key '= "") { 117 | set lb = $GET(^WebTerminal("AuthUser", key)) 118 | if ((lb '= "") && (key = authKey)) { 119 | set list = lb 120 | } 121 | set time = $LISTGET(lb, 2) 122 | if (time '= "") && ($System.SQL.DATEDIFF("s", time, $h) > ..#WSKEYEXPIRES) { 123 | kill ^WebTerminal("AuthUser", key) 124 | } 125 | set key = $ORDER(^WebTerminal("AuthUser", key)) 126 | } 127 | 128 | if (list = "") { // not found 129 | return $LB("Invalid key") 130 | } 131 | 132 | set username = $LISTGET(list, 1) 133 | set namespace = $LISTGET(list, 3) 134 | set ns = $Namespace 135 | 136 | znspace "%SYS" 137 | do ##class(Security.Users).Get(username, .userProps) 138 | znspace ns 139 | 140 | set namespace = $case(namespace, "":userProps("NameSpace"), :namespace) 141 | 142 | if ($get(userProps("Routine")) '= "") { 143 | set ..StartupRoutine = userProps("Routine") 144 | } 145 | 146 | if $get(userProps("Enabled")) '= 1 { 147 | return $LB("User " _ username _ " is not enabled in the system") 148 | } 149 | 150 | set $LIST(list, 3) = namespace 151 | set loginStatus = $System.Security.Login(username) 152 | 153 | if (loginStatus '= 1) { 154 | return $LB($System.Status.GetErrorText(loginStatus)) 155 | } 156 | 157 | return $LB("", list) 158 | } 159 | 160 | /// See WebTerminal.Handlers 161 | Method ProcessRequest(handler As %String, data) As %Status [ Private ] 162 | { 163 | try { 164 | return $CLASSMETHOD("WebTerminal.Handlers", handler, $this, data) 165 | } catch (e) { 166 | set ..echo = 1 167 | return e.AsSystemError() 168 | } 169 | } 170 | 171 | /// Main method for every new client. 172 | Method ClientLoop() As %Status [ Private ] 173 | { 174 | job ##class(WebTerminal.Core).Loop(..StartupRoutine):($NAMESPACE):5 175 | if ($TEST '= 1) { // $TEST=0 for JOB only when timeouted 176 | do ..Send("error", "%noJob") 177 | return $$$NOTOK 178 | } 179 | set ..corePID = $ZCHILD 180 | set ..childNamespace = $NAMESPACE 181 | if (..StartupRoutine = "") { 182 | do ..Send("prompt", ..childNamespace) 183 | } else { 184 | set message = ##class(%ZEN.proxyObject).%New() 185 | set status = $CLASSMETHOD("WebTerminal.Handlers", "Execute", $this, "", 1) 186 | goto loopEnd 187 | } 188 | //try { // temp 189 | for { 190 | set message = ..GetMessage() 191 | quit:(message = "") // if client is gone, finish looping 192 | if (message.error '= "") { 193 | if (message.error '[ "ERROR #7951") { // don't try and send message if it was a WS close error 194 | set st = ..Send("error", message.error) 195 | } 196 | quit 197 | } 198 | if (message."_cb" '= "") { set ..handler = message."_cb" } 199 | set status = ..ProcessRequest(message.h, message.d) 200 | set ..handler = 0 201 | set ..echo = 1 202 | if (status '= "") && (status '= $$$OK) { 203 | set eType = $EXTRACT(status, 1, 1) 204 | do ..Send("oLocalized", $C(13,10) _ $case(eType = 0, 1: $System.Status.GetErrorText(status), :status)) 205 | continue 206 | } 207 | } 208 | loopEnd 209 | //} catch (e) { do ..Send("o", $System.Status.GetErrorText(e)) } // temp 210 | return $$$OK 211 | } 212 | 213 | /// This method sends basic login info to the user. Use this method to set client variables 214 | /// during the WebTerminal initialization. 215 | /// authList See Router.Auth method. 216 | Method SendLoginInfo(authList As %List) 217 | { 218 | set obj = ##class(%ZEN.proxyObject).%New() 219 | set obj.username = $USERNAME 220 | set obj.name = $get(^WebTerminal("Name")) 221 | set obj.cleanStart = $ListGet(authList, 4) 222 | set obj.system = $SYSTEM 223 | set obj.firstLaunch = ($get(^WebTerminal("FirstLaunch"), 1) '= 0) 224 | set obj.InstanceGUID = ##class(%SYS.System).InstanceGUID() 225 | set obj.zv = $ZVersion 226 | set ^WebTerminal("FirstLaunch") = 0 227 | do ..Send("init", obj) 228 | } 229 | 230 | /// Triggered when new connection established. 231 | Method Server() As %Status 232 | { 233 | set authRes = ..RequireAuthorization() 234 | set authMessage = $ListGet(authRes, 1) 235 | if (authMessage = "") { 236 | set authList = $ListGet(authRes, 2) // see Router.Auth method 237 | set namespace = $ListGet(authList, 3) 238 | if (namespace '= "") && (namespace '= $Namespace) { 239 | try { 240 | znspace namespace 241 | } catch (e) { 242 | do ..Send("oLocalized", 243 | $Char(27) _ "[31m%unNS(" _ namespace _ ")"_ $Char(27) _ "[0m" _ $Char(13,10) 244 | ) 245 | } 246 | } 247 | set ..CurrentNamespace = $Namespace 248 | do ..SendLoginInfo(authList) 249 | do ..ClientLoop() 250 | do ..Send("oLocalized", "%wsNormalClose"_$C(13,10)) 251 | } else { 252 | do ..Send("oLocalized", "%wsRefuse(" _ authMessage _ ")") 253 | } 254 | do ..EndServer() 255 | set %session.EndSession = 1 256 | quit $$$OK 257 | } 258 | 259 | } 260 | -------------------------------------------------------------------------------- /src/cls/WebTerminal/ErrorDecomposer.cls: -------------------------------------------------------------------------------- 1 | Class WebTerminal.ErrorDecomposer 2 | { 3 | 4 | Parameter LINES As %Numeric = 5; 5 | 6 | /// Takes $ZERROR function result. 7 | /// Returns either simple string or %ZEN.proxyObject representing the error details. 8 | ClassMethod DecomposeError (err As %String = "", ns As %String = "") 9 | { 10 | new $namespace 11 | if (ns '= "") { 12 | try { 13 | set $namespace = ns 14 | } catch (e) { 15 | return err 16 | } 17 | } 18 | return:($FIND(err, "<") '= 2) err 19 | set startPos = $FIND(err, ">") 20 | return:(startPos = 0) err 21 | set spacePos = $FIND(err, " ") - 1 22 | return:(spacePos = startPos) err 23 | set label = $EXTRACT(err, startPos, $case(spacePos = -1, 1:999, :spacePos-1)) 24 | return:(label = "") err 25 | try { 26 | set obj = ##class(%ZEN.proxyObject).%New() 27 | set obj.zerror = err 28 | set plusPos = $FIND(label, "+") 29 | set cPos = $FIND(label, "^") 30 | if (plusPos = 0) || (cPos = 0) { 31 | set obj.source = $TEXT(@label) 32 | set obj.line = 0 33 | return obj 34 | } 35 | set line = +$EXTRACT(label, plusPos, cPos - 2) 36 | set part1 = $EXTRACT(label, 1, plusPos - 1) 37 | set part2 = $EXTRACT(label, cPos - 1, *) 38 | set range = ..#LINES \ 2 39 | set obj.source = "" 40 | set obj.line = 0 41 | for i=line-range:1:line+range { 42 | continue:(i < 1) 43 | set label = part1 _ i _ part2 44 | set text = $TEXT(@label) 45 | set:(text '= "") obj.source = obj.source _ $case(obj.source = "", 1: "", :$C(10)) _ text 46 | set:((text '= "") && (i < line)) obj.line = obj.line + 1 47 | } 48 | return obj 49 | } catch (e) { 50 | return err 51 | } 52 | return err 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/cls/WebTerminal/Router.cls: -------------------------------------------------------------------------------- 1 | /// The REST interface: class that routes HTTP requests 2 | Class WebTerminal.Router Extends %CSP.REST [ CompileAfter = StaticContent ] 3 | { 4 | 5 | XData UrlMap 6 | { 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | } 16 | 17 | /// Calls StaticContent.Write method or sends not modified header. Type have to be "css" or "js" 18 | ClassMethod WriteStatic(type As %String, ContentType As %String = "") [ Private ] 19 | { 20 | #define CompileTime ##Expression("""" _ $zd($h, 11) _ ", "_ $zdt($NOW(0), 2,1) _ " GMT""") 21 | set %response.CharSet = "utf-8" 22 | set %response.ContentType = $case(type, 23 | "css": "text/css", 24 | "js": "text/javascript", 25 | "html": "text/html", 26 | : $case(ContentType="", 1:"text/plain", :ContentType) 27 | ) 28 | do %response.SetHeader("Last-Modified", $$$CompileTime) 29 | 30 | if (%request.GetCgiEnv("HTTP_IF_MODIFIED_SINCE")=$$$CompileTime) { 31 | set %response.Status = "304 Not Modified" 32 | } else { 33 | do ##class(StaticContent).Write(type) 34 | } 35 | } 36 | 37 | ClassMethod Auth() As %Status 38 | { 39 | set cookie = $System.Encryption.Base64Encode(%session.Key) 40 | set ^WebTerminal("AuthUser", cookie) = $LB( // authList 41 | $Username, // username 42 | $Horolog, // granting ticket date 43 | $Get(%request.Data("ns", 1), $Get(%request.Data("NS", 1))), 44 | $Get(%request.Data("clean", 1), 0) '= 0 45 | ) 46 | write "{""key"":""" _ cookie _ """}" 47 | return $$$OK 48 | } 49 | 50 | /// Method writes application CSS. 51 | ClassMethod GetCss() As %Status 52 | { 53 | do ..WriteStatic("css") 54 | return $$$OK 55 | } 56 | 57 | /// Method writes application theme. 58 | ClassMethod GetTheme(Theme As %String) As %Status 59 | { 60 | do ..WriteStatic("Theme"_$REPLACE(Theme, ".css", ""),"text/css") 61 | return $$$OK 62 | } 63 | 64 | /// Method writes application JavaScript. 65 | ClassMethod GetJs() As %Status 66 | { 67 | do ..WriteStatic("js") 68 | return $$$OK 69 | } 70 | 71 | /// Method writes application HTML. 72 | ClassMethod Index() As %Status 73 | { 74 | do ..WriteStatic("html") 75 | return $$$OK 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/cls/WebTerminal/StaticContent.cls: -------------------------------------------------------------------------------- 1 | /// This class holds whole application static content like scripts and styles. 2 | /// Do not edit this file - use external tool to generate it. 3 | Class WebTerminal.StaticContent 4 | { 5 | 6 | /// Write the contents of xData tag 7 | ClassMethod Write(Const As %String) As %Status 8 | { 9 | set obj = ##class(%Dictionary.CompiledXData).%OpenId("WebTerminal.StaticContent||"_Const) 10 | return:(obj = "") $$$OK 11 | set xdata = obj.Data 12 | set status = ##class(%XML.TextReader).ParseStream(xdata, .textreader) 13 | while textreader.Read() { if (textreader.NodeType="chars") { 14 | write textreader.Value 15 | } } 16 | return $$$OK 17 | } 18 | 19 | XData Themecache 20 | { 21 | 22 | ]]> 23 | 24 | } 25 | 26 | XData html 27 | { 28 | 29 | ]]> 30 | 31 | } 32 | 33 | XData css 34 | { 35 | 36 | ]]> 37 | 38 | } 39 | 40 | XData js 41 | { 42 | 43 | ]]> 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/cls/WebTerminal/Trace.cls: -------------------------------------------------------------------------------- 1 | Class WebTerminal.Trace Extends Common 2 | { 3 | 4 | /// Property is used to store watching files/globals. 5 | Property Watches As %List; 6 | 7 | /// Watch position in file or global 8 | Property WatchesCaret As %Numeric [ MultiDimensional ]; 9 | 10 | /// Checks for correct watch source and sets watch target to ..Watches 11 | /// Returns status of this operation 12 | Method Trace(name) As %Status 13 | { 14 | set s = $CHAR(0) 15 | set watches = s _ $LISTTOSTRING(..Watches, s) _ s 16 | if ($FIND(watches, s_name_s) '= 0) return 0 // if watch already defined 17 | 18 | if ($EXTRACT(name,1,1) = "^") { // watching global 19 | set g = 0 20 | try { 21 | if (($data(@name)) '= 0) set g = 1 22 | } catch { } 23 | set $ZERROR = "" 24 | if (g = 1) { 25 | set ..Watches = ..Watches _ $LISTBUILD(name) 26 | set ..WatchesCaret(name, 0) = $QUERY(@name@(""), -1) // last 27 | set ..WatchesCaret(name, 1) = "?" 28 | return 1 29 | } 30 | } else { // watch file 31 | if (##class(%File).Exists(name)) { 32 | set ..Watches = ..Watches _ $LISTBUILD(name) 33 | set file = ##class(%File).%New(name) 34 | set ..WatchesCaret(name,0) = file.Size // current watch cursor position 35 | set ..WatchesCaret(name,1) = file.DateModified 36 | return 1 37 | } 38 | } 39 | 40 | return 0 41 | } 42 | 43 | /// Removes watch from watches list 44 | /// Returns success status 45 | Method StopTracing(name) As %Status 46 | { 47 | set s = $CHAR(0) 48 | set watches = s _ $LISTTOSTRING(..Watches,s) _ s 49 | set newWatches = $REPLACE(watches, s_name_s, s) 50 | set ..Watches = $LISTFROMSTRING($EXTRACT(newWatches, 2, *-1), s) 51 | if (watches '= newWatches) { 52 | kill ..WatchesCaret(name) 53 | } 54 | return watches '= newWatches 55 | } 56 | 57 | /// Returns a list current watches 58 | Method ListWatches() As %String 59 | { 60 | set no = 0 61 | set s = "Watching: " _ $CHAR(10) 62 | while $LISTNEXT(..Watches, no, value) { 63 | set s = s_"(pos: "_..WatchesCaret(value,0)_ 64 | "; mod: "_..WatchesCaret(value,1)_") "_value_$CHAR(10) 65 | } 66 | return s 67 | } 68 | 69 | /// Return null string if global hadn't been updated 70 | /// This method watches only for tail of global and detects if global still alive 71 | Method GetTraceGlobalModified(watch) As %List 72 | { 73 | set data = "" 74 | if ($data(@watch)=0) { 75 | do ..StopTracing(watch) 76 | return $lb($C(27)_"[(wrong)m[D]"_$C(27)_"[0m", $C(13, 10)) 77 | } 78 | for { 79 | set query = $QUERY(@..WatchesCaret(watch,0)) 80 | quit:query="" 81 | set ..WatchesCaret(watch,0) = query 82 | set data = data _ $case(data = "", 1: "", :$CHAR(13, 10)) _ @query 83 | } 84 | return $lb($C(27)_"[(special)m[M]"_$C(27)_"[0m", data) 85 | } 86 | 87 | Method GetTraceFileModified(watch) As %String 88 | { 89 | set file=##class(%File).%New(watch) 90 | set size = file.Size 91 | set modDate = file.DateModified 92 | set output = "" 93 | if (size < 0) { // file had been deleted 94 | do ..StopTracing(watch) 95 | return $lb($C(27)_"[(wrong)m[D]"_$C(27)_"[0m", $C(13, 10)) 96 | } 97 | 98 | if (size > ..WatchesCaret(watch, 0)) { 99 | 100 | set stream = ##class(%Stream.FileCharacter).%New() 101 | set sc = stream.LinkToFile(watch) 102 | do stream.MoveTo(..WatchesCaret(watch, 0) + 1) 103 | set read = stream.Read(size - ..WatchesCaret(watch, 0)) 104 | set output = output _ read 105 | set ..WatchesCaret(watch, 0) = size 106 | set ..WatchesCaret(watch, 1) = file.DateModified 107 | return $lb($C(27)_"[(constant)m[A]"_$C(27)_"[0m", output) 108 | 109 | } elseif ((size < ..WatchesCaret(watch, 0)) || (file.DateModified '= ..WatchesCaret(watch, 1))) { 110 | 111 | set output = output _ "Size change: " _ (size - ..WatchesCaret(watch, 0)) 112 | set ..WatchesCaret(watch, 0) = size 113 | set ..WatchesCaret(watch, 1) = file.DateModified 114 | return $lb($C(27)_"[(special)m[M]"_$C(27)_"[0m", output) 115 | 116 | } // else file not changed 117 | 118 | return $lb("", "") 119 | } 120 | 121 | Method CheckTracing() As %String 122 | { 123 | set no = 0 124 | set data = "" 125 | set overall = "" 126 | set watchList = ..Watches // do not remove or simplify: ..Watches can be modified 127 | while $LISTNEXT(watchList, no, value) { 128 | set global = $EXTRACT(value, 1, 1) = "^" 129 | if global { 130 | set data = ..GetTraceGlobalModified(value) 131 | } else { 132 | set data = ..GetTraceFileModified(value) 133 | } 134 | if ($LISTGET(data, 2) '= "") { 135 | set overall = $LISTGET(data, 1) _ " " _ $C(27) _ "[2m" _ $ZDATETIME($NOW(),1,1) 136 | _ $C(27) _ "[0m " _ $C(27) _ "[(" _ $case(global, 1: "global", :"string") _ ")m" 137 | _ value _ $C(27) _ "[0m" _ $CHAR(13, 10) _ $LISTGET(data, 2) _ $CHAR(13, 10) 138 | } 139 | set data = "" 140 | } 141 | return overall 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/cls/WebTerminal/Updater.cls: -------------------------------------------------------------------------------- 1 | /// version update module class. 2 | /// This class represents update mechanism for WebTerminal. Internet connection is required to 3 | /// update WebTerminal. 4 | Class WebTerminal.Updater 5 | { 6 | 7 | /// SSL configuration name used for HTTPS requests. 8 | Parameter SSLConfigName = "WebTerminalSSL"; 9 | 10 | ClassMethod GetSSLConfigurationName () As %String 11 | { 12 | new $namespace 13 | zn "%SYS" 14 | if ('##class(Security.SSLConfigs).Exists(..#SSLConfigName)) { 15 | set st = ##class(Security.SSLConfigs).Create(..#SSLConfigName) 16 | return:(st '= 1) "UnableToCreateSSLConfig:"_$System.Status.GetErrorText(st) 17 | } 18 | return ..#SSLConfigName 19 | } 20 | 21 | ClassMethod WriteAndDelete (client As WebTerminal.Engine, file As %String) As %Status 22 | { 23 | if ##class(%File).Exists(file) { 24 | set stream = ##class(%Stream.FileCharacter).%New() 25 | set sc = stream.LinkToFile(file) 26 | while 'stream.AtEnd { 27 | do client.Send("o", stream.Read()_$CHAR(13, 10)) 28 | } 29 | do client.Send("oLocalized", "%sUpdCleanLog("_file_")"_$CHAR(13, 10)) 30 | do ##class(%File).Delete(file) 31 | } else { 32 | do client.Send("oLocalized", "%sUpdNoFile") 33 | } 34 | return $$$OK 35 | } 36 | 37 | ClassMethod Stop (client As WebTerminal.Engine, status As %Status) As %Status 38 | { 39 | if ($$$ISERR(status)) { 40 | do client.Send("oLocalized", "%sUpdErr("_$System.Status.GetErrorText(status)_")"_$C(13, 10)) 41 | } 42 | return status 43 | } 44 | 45 | ClassMethod Update (client As WebTerminal.Engine, URL As %String) As %Status 46 | { 47 | do client.Send("oLocalized", "%sUpdSt"_$CHAR(13, 10)) 48 | set request = ##class(%Net.HttpRequest).%New() 49 | set request.Server = $PIECE(URL, "/", 3) 50 | set request.Location = $PIECE(URL, "/", 4, *) 51 | set request.Https = 1 52 | set request.SSLConfiguration = ..GetSSLConfigurationName() 53 | do client.Send("oLocalized", "%sUpdRURL(https://"_request.Server_"/"_request.Location_")"_$CHAR(13, 10)) 54 | set status = request.Get() 55 | do client.Send("oLocalized", "%sUpdGetOK"_$CHAR(13, 10)) 56 | return:(status '= $$$OK) status 57 | 58 | if (request.HttpResponse.StatusCode '= 200) { 59 | do client.Send("oLocalized", "%sUpdSCode("_request.HttpResponse.StatusCode_")"_$CHAR(13, 10)) 60 | return $$$ERROR($$$GeneralError, "HTTP "_request.HttpResponse.StatusCode) 61 | } 62 | 63 | do client.Send("oLocalized", "%sUpdWTF"_$CHAR(13, 10)) 64 | set tempFile = ##class(%File).TempFilename("xml") 65 | set file = ##class(%File).%New(tempFile) 66 | do file.Open("NW") 67 | set data = request.HttpResponse.Data 68 | if (($IsObject(data)) && (data.%IsA("%Stream.Object"))) { 69 | while 'data.AtEnd { 70 | set chunk = data.Read(data.Size) 71 | do file.Write(chunk) 72 | } 73 | } else { 74 | do file.Write(data) 75 | } 76 | do file.Close() 77 | 78 | set backupFile = ##class(%File).TempFilename("xml") 79 | do client.Send("oLocalized", "%sUpdBack("_backupFile_")"_$CHAR(13, 10)) 80 | 81 | set logFile = ##class(%File).TempFilename("txt") 82 | set io = $IO 83 | 84 | open logFile:("NW") 85 | use logFile 86 | set exportStatus = $System.OBJ.Export("WebTerminal.*.CLS", backupFile) 87 | close logFile 88 | use io 89 | 90 | do ..WriteAndDelete(client, logFile) 91 | if ($$$ISERR(exportStatus)) { 92 | do ##class(WebTerminal.Analytics).ReportInstallStatus(exportStatus, "Update") 93 | return ..Stop(client, exportStatus) 94 | } 95 | do client.Send("oLocalized", "%sUpdRemLoad"_$CHAR(13, 10)) 96 | 97 | open logFile:("NW") 98 | use logFile 99 | write $C(27)_"[2m" 100 | do $System.OBJ.DeletePackage("WebTerminal") 101 | write $C(27)_"[0m", ! 102 | set loadStatus = $SYSTEM.OBJ.Load(tempFile, "c") 103 | 104 | // At this moment WebTerminal's code can be broken, totally changed or deleted. Do not call 105 | // WebTerminal's methods until terminal is restored / fully updated 106 | 107 | if '$$$ISOK(loadStatus) { // roll back 108 | write !, $C(27)_"[(wrong)m==FAILED=="_$C(27)_"[0m", !, 109 | $System.Status.GetErrorText(loadStatus), !, !, 110 | $C(27)_"[(special)m==RESTORING=="_$C(27)_"[0m", ! 111 | do $SYSTEM.OBJ.Load(backupFile, "c") 112 | } 113 | 114 | // end 115 | 116 | close logFile 117 | use io 118 | do ..WriteAndDelete(client, logFile) 119 | 120 | do client.Send("oLocalized", "%sUpdClean("_tempFile_")"_$CHAR(13, 10)) 121 | do ##class(%File).Delete(tempFile) 122 | do client.Send("oLocalized", "%sUpdClean("_backupFile_")"_$CHAR(13, 10, 13, 10)) 123 | do ##class(%File).Delete(backupFile) 124 | if '$$$ISOK(loadStatus) { 125 | do ..Stop(client, loadStatus) 126 | do client.Send("oLocalized", $CHAR(13, 10)_"%sUpdRes"_$CHAR(13, 10)) 127 | } 128 | do client.Send("oLocalized", "%sUpdDone"_$CHAR(13, 10)) 129 | 130 | do ##class(WebTerminal.Analytics).ReportInstallStatus(loadStatus, "Update") 131 | 132 | return $$$OK 133 | 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /src/mac/WebTerminal/EscapeSequencesTest.mac: -------------------------------------------------------------------------------- 1 | /// This routine demonstrates Caché terminal escape sequences support. 2 | /// Created as standard for Caché WEB Terminal v2 3 | EscapeSequencesDemo 4 | s ESC = $c(27) 5 | s standardAttributes = $lb("Bright", "Dim", "", "Underscore", "Blink", "", "Reverse", "Hidden") 6 | s foregroundColors = $lb("Black", "Red", "Green", "Yellow", "Blue", "Magenta", "Cyan", "White") 7 | s backgroundColors = $lb("Black", "Red", "Green", "Yellow", "Blue", "Magenta", "Cyan", "White") 8 | 9 | w # 10 | w "Screen cleared.", ! 11 | 12 | w !, "Standard attributes:", ! 13 | for i=1:1:8 { 14 | if ($lg(standardAttributes, i) = "") continue 15 | w "[" , i, "m", ": ", ESC, "[", i, "m", $lg(standardAttributes, i), ESC, "[0m", " " 16 | } 17 | 18 | w !!, "Foreground colors:", ! 19 | for i=30:1:37 { 20 | if ($lg(foregroundColors, i-29) = "") continue 21 | w "[" , i, "m", ": ", ESC, "[", i, "m", $lg(foregroundColors, i-29), ESC, "[0m", " " 22 | } 23 | 24 | w !!, "Background colors:", ! 25 | for i=40:1:47 { 26 | if ($lg(backgroundColors, i-39) = "") continue 27 | w "[" , i, "m", ": ", ESC, "[", i, "m", $lg(backgroundColors, i-39), ESC, "[0m", " " 28 | } 29 | 30 | w !!, "Combinations:" 31 | w !, ESC, "[31;1;46;4m", "Bright red underscored text on cyan background", ESC, "[0m", ! 32 | w ESC, "[5;34;42;4m", "Blue blinking text on green background", ESC, "[0m", ! 33 | 34 | d Read 35 | w # 36 | 37 | w !!, "Scrolling sequences.", !, "This", !, "Text", !, "Must", !, "Be", !, "Overwritten", ! 38 | w ESC, "[18;22r" 39 | 40 | for i=0:1:40 { 41 | if (i = 16) w ESC, "[4m" 42 | w "Line --------------- ", i, ! 43 | if (i = 16) w ESC, "[0m" 44 | } 45 | 46 | w !, "Here display scrolls. Next: \r, scroll 16 up, 1 down." 47 | r null 48 | w $c(13), ESC, "M", ESC, "M", ESC, "M", ESC, "M", 49 | ESC, "M", ESC, "M", ESC, "M", ESC, "M", 50 | ESC, "M", ESC, "M", ESC, "M", ESC, "M", 51 | ESC, "M", ESC, "M", ESC, "M", ESC, "M", ESC, "D" 52 | 53 | w "Next: enable scrolling for entire display and clearing the screen." 54 | d Read 55 | w ESC, "[r", # 56 | 57 | w !!, "Tab control: clear all tabs, set new tabs.", !! 58 | w ESC, "[3g" 59 | w " -1->", ESC, "H", " ---2--->", ESC, "H", " --------3------->", ESC, "H", " ---4--->", ESC, "H", 60 | " -------5------->", ESC, "H", ! 61 | w $c(9), "1", $c(9), "2", $c(9), "3", $c(9), "4", $c(9), "5", ! 62 | 63 | w "clear tab position 3 ---------->", ESC, "[g", ! 64 | w $c(9), "1", $c(9), "2", $c(9), "3", $c(9), "4", $c(9), "5", ! 65 | d Read 66 | 67 | w !!, "Device status queries:" 68 | w !, "[c (Querying device code): ", ESC, "[c" 69 | r temp 70 | w "RESPONSE: ", $replace(temp, ESC, ""), ";" 71 | w !, "[5n (Querying device status): ", ESC, "[5n" 72 | r temp 73 | w "RESPONSE: ", $replace(temp, ESC, ""), "; ([0n = OK, [3n = FAIL)" 74 | w !, "[6n (Querying cursor position): ", ESC, "[6n" 75 | r temp 76 | w "RESPONSE: ", $replace(temp, ESC, ""), ";" 77 | // it seems that Caché term isn't responding to this requests 78 | 79 | // does this affects to something? 80 | w !!, "Resetting device...", ESC, "c" 81 | d Read 82 | 83 | // Not effective for Caché term: 84 | // [7l (disable line wrap) 85 | // [7h (enable line wrap) 86 | 87 | w #, !!, "Cursor control: move (38, 10), draw box." 88 | w ESC, "[10;38H", "XX", ESC, "[1C", "XX", ESC, "[1B", ESC, "[1D", "X", 89 | ESC, "[1B", ESC, "[1D", "X", ESC, "[5D", "XXXX", ESC, "[4D", ESC, "[1A", "X" 90 | // Not effective for Caché term: 91 | // [s (save cursor) 92 | // [u (restore cursor) 93 | // [7 (save cursor with attributes) 94 | // [8 (restore cursor with attributes) 95 | 96 | w !!, "this text", !!!, "will (except this line) be", !!!, 97 | "e ", $c(10), "r ", $c(10), "a ", $c(10), "z ", $c(10), "e ", 98 | $c(10), "d" 99 | d Read 100 | 101 | w ESC, "[1A", ESC, "[K" // erase "d" letter (till end of a line) 102 | w ESC, "[2A", ESC, "[40C", "X", ESC, "[1K" // erase "z" letter (till start of a line) 103 | w ESC, "[1B", ESC, "[2K" // erase "e" letter (whole line) 104 | w ESC, "[5A", ESC, "[J" // erase "a", "r", "e" (to bottom) 105 | w ESC, "[5A", ESC, "[1J" // erase screen to home except one line 106 | w ESC, "[1;1H", "Erasing whole screen and finishing program." 107 | d Read 108 | 109 | w ESC, "[2J", !, "Done." 110 | 111 | // check printing support (when printer is connected) 112 | // define key support? 113 | 114 | q 115 | 116 | Read() 117 | w !, "Press ENTER to continue..." 118 | r null 119 | q --------------------------------------------------------------------------------