diff --git a/.gitignore b/.gitignore index f3954dc64..e29881073 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ DEBUG *.fasl node_modules -package-lock.json roswell/lem-ncurses *~ @@ -27,7 +26,7 @@ GIT_IGNORE.* lem lem-ncurses -lem-rpc +lem-server *.abcl build/* diff --git a/Makefile b/Makefile index 6965a9fb9..495a64be3 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,10 @@ sdl2-ncurses: qlot install $(LISP) --noinform --no-sysinit --no-userinit --load .qlot/setup.lisp --load scripts/build-sdl2-ncurses.lisp +server: + qlot install + $(LISP) --noinform --no-sysinit --no-userinit --load .qlot/setup.lisp --load scripts/build-server.lisp + install: qlot install $(LISP) --noinform --no-sysinit --no-userinit --load .qlot/setup.lisp --load scripts/build-sdl2-ncurses.lisp diff --git a/frontends/server/config.lisp b/frontends/server/config.lisp new file mode 100644 index 000000000..3a80f6ef5 --- /dev/null +++ b/frontends/server/config.lisp @@ -0,0 +1,3 @@ +(in-package :cl-user) + +(setf lem-markdown-mode/internal::*view-type* :html-buffer) diff --git a/frontends/server/frontend/.gitignore b/frontends/server/frontend/.gitignore new file mode 100644 index 000000000..a9b26d1fe --- /dev/null +++ b/frontends/server/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +#dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontends/server/frontend/dist/assets/index-DkE9_tVc.js b/frontends/server/frontend/dist/assets/index-DkE9_tVc.js new file mode 100644 index 000000000..392d4699e --- /dev/null +++ b/frontends/server/frontend/dist/assets/index-DkE9_tVc.js @@ -0,0 +1 @@ +(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))i(o);new MutationObserver(o=>{for(const n of o)if(n.type==="childList")for(const s of n.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&i(s)}).observe(document,{childList:!0,subtree:!0});function t(o){const n={};return o.integrity&&(n.integrity=o.integrity),o.referrerPolicy&&(n.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?n.credentials="include":o.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function i(o){if(o.ep)return;o.ep=!0;const n=t(o);fetch(o.href,n)}})();var commonjsGlobal=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},dist={},client={},models={};(function(r){var e=commonjsGlobal&&commonjsGlobal.__extends||function(){var d=function(f,p){return d=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(m,y){m.__proto__=y}||function(m,y){for(var v in y)Object.prototype.hasOwnProperty.call(y,v)&&(m[v]=y[v])},d(f,p)};return function(f,p){if(typeof p!="function"&&p!==null)throw new TypeError("Class extends value "+String(p)+" is not a constructor or null");d(f,p);function m(){this.constructor=f}f.prototype=p===null?Object.create(p):(m.prototype=p.prototype,new m)}}();Object.defineProperty(r,"__esModule",{value:!0}),r.createJSONRPCNotification=r.createJSONRPCRequest=r.createJSONRPCSuccessResponse=r.createJSONRPCErrorResponse=r.JSONRPCErrorCode=r.JSONRPCErrorException=r.isJSONRPCResponses=r.isJSONRPCResponse=r.isJSONRPCRequests=r.isJSONRPCRequest=r.isJSONRPCID=r.JSONRPC=void 0,r.JSONRPC="2.0";var t=function(d){return typeof d=="string"||typeof d=="number"||d===null};r.isJSONRPCID=t;var i=function(d){return d.jsonrpc===r.JSONRPC&&d.method!==void 0&&d.result===void 0&&d.error===void 0};r.isJSONRPCRequest=i;var o=function(d){return Array.isArray(d)&&d.every(r.isJSONRPCRequest)};r.isJSONRPCRequests=o;var n=function(d){return d.jsonrpc===r.JSONRPC&&d.id!==void 0&&(d.result!==void 0||d.error!==void 0)};r.isJSONRPCResponse=n;var s=function(d){return Array.isArray(d)&&d.every(r.isJSONRPCResponse)};r.isJSONRPCResponses=s;var l=function(d,f,p){var m={code:d,message:f};return p!=null&&(m.data=p),m},c=function(d){e(f,d);function f(p,m,y){var v=d.call(this,p)||this;return Object.setPrototypeOf(v,f.prototype),v.code=m,v.data=y,v}return f.prototype.toObject=function(){return l(this.code,this.message,this.data)},f}(Error);r.JSONRPCErrorException=c,function(d){d[d.ParseError=-32700]="ParseError",d[d.InvalidRequest=-32600]="InvalidRequest",d[d.MethodNotFound=-32601]="MethodNotFound",d[d.InvalidParams=-32602]="InvalidParams",d[d.InternalError=-32603]="InternalError"}(r.JSONRPCErrorCode||(r.JSONRPCErrorCode={}));var a=function(d,f,p,m){return{jsonrpc:r.JSONRPC,id:d,error:l(f,p,m)}};r.createJSONRPCErrorResponse=a;var u=function(d,f){return{jsonrpc:r.JSONRPC,id:d,result:f??null}};r.createJSONRPCSuccessResponse=u;var h=function(d,f,p){return{jsonrpc:r.JSONRPC,id:d,method:f,params:p}};r.createJSONRPCRequest=h;var N=function(d,f){return{jsonrpc:r.JSONRPC,method:d,params:f}};r.createJSONRPCNotification=N})(models);var internal={};Object.defineProperty(internal,"__esModule",{value:!0});internal.DefaultErrorCode=void 0;internal.DefaultErrorCode=0;var __awaiter$2=commonjsGlobal&&commonjsGlobal.__awaiter||function(r,e,t,i){function o(n){return n instanceof t?n:new t(function(s){s(n)})}return new(t||(t=Promise))(function(n,s){function l(u){try{a(i.next(u))}catch(h){s(h)}}function c(u){try{a(i.throw(u))}catch(h){s(h)}}function a(u){u.done?n(u.value):o(u.value).then(l,c)}a((i=i.apply(r,e||[])).next())})},__generator$2=commonjsGlobal&&commonjsGlobal.__generator||function(r,e){var t={label:0,sent:function(){if(n[0]&1)throw n[1];return n[1]},trys:[],ops:[]},i,o,n,s;return s={next:l(0),throw:l(1),return:l(2)},typeof Symbol=="function"&&(s[Symbol.iterator]=function(){return this}),s;function l(a){return function(u){return c([a,u])}}function c(a){if(i)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(t=0)),t;)try{if(i=1,o&&(n=a[0]&2?o.return:a[0]?o.throw||((n=o.return)&&n.call(o),0):o.next)&&!(n=n.call(o,a[1])).done)return n;switch(o=0,n&&(a=[a[0]&2,n.value]),a[0]){case 0:case 1:n=a;break;case 4:return t.label++,{value:a[1],done:!1};case 5:t.label++,o=a[1],a=[0];continue;case 7:a=t.ops.pop(),t.trys.pop();continue;default:if(n=t.trys,!(n=n.length>0&&n[n.length-1])&&(a[0]===6||a[0]===2)){t=0;continue}if(a[0]===3&&(!n||a[1]>n[0]&&a[1]0&&n[n.length-1])&&(a[0]===6||a[0]===2)){t=0;continue}if(a[0]===3&&(!n||a[1]>n[0]&&a[1]0&&n[n.length-1])&&(a[0]===6||a[0]===2)){t=0;continue}if(a[0]===3&&(!n||a[1]>n[0]&&a[1]{const[t,i,o]=e;this.requestInternal(t,i,o)}),this.messageQueue=[]}request(e,t,i){this.webSocket.readyState===WebSocket.OPEN?this.requestInternal(e,t,i):this.messageQueue.push([e,t,i])}notify(e,t){switch(this.webSocket.readyState){case WebSocket.OPEN:this.serverAndClient.notify(e,t);break}}connect(e){this.closed||(console.log("connect",this.url),this.webSocket=new WebSocket(this.url),this.serverAndClient||(this.serverAndClient=new dist.JSONRPCServerAndClient(new dist.JSONRPCServer,new dist.JSONRPCClient(t=>{try{return this.webSocket.send(JSON.stringify(t)),Promise.resolve()}catch(i){return Promise.reject(i)}}))),this.webSocket.onmessage=t=>{this.serverAndClient.receiveAndSend(JSON.parse(t.data.toString()))},this.webSocket.onopen=()=>{console.log("WebSocket connection established"),this.connectionEstablished=!0,this.onConnected&&this.onConnected(),this.requestMessageQueue()},this.webSocket.onclose=t=>{console.error("WebScoket closed",t),this.serverAndClient.rejectAllPendingRequests(`Connection is closed (${t.reason}).`),this.connectionEstablished&&this.onClosed(),this.timerId=setTimeout(()=>{this.connect()},3e3)},this.webSocket.onerror=t=>{console.error("WebSocket error:",t),this.webSocket.close()})}}const modifierKeys=["Shift","Control","Alt","Meta"],convertKeyTable={Enter:"Return",ArrowRight:"Right",ArrowLeft:"Left",ArrowUp:"Up",ArrowDown:"Down","¡":"1","™":"2","£":"3","¢":"4","∞":"5","§":"6","¶":"7","•":"8",ª:"9",º:"0","–":"-","≠":"=","“":"[","‘":"]","«":"\\","…":";",æ:"'","≤":",","≥":".","÷":"/","⁄":"!","€":"@","‹":"#","›":"$",fi:"%",fl:"^","‡":"&","°":"*","·":"(","‚":")","—":"_","±":"+","”":"{","’":"}","»":"|",Ú:":",Æ:'"',"¯":"<","˘":">","¿":"?",œ:"q","∑":"w","´":"e","®":"r","†":"t","¥":"y","¨":"u","ˆ":"i",ø:"o",π:"p",å:"a",ß:"s","∂":"d",ƒ:"f","©":"g","˙":"h","∆":"j","˚":"k","¬":"l",Ω:"z","≈":"x",ç:"c","√":"v","∫":"b","˜":"n",µ:"m",Œ:"Q","„":"W","´":"E","‰":"R","ˇ":"T",Á:"Y","¨":"U","ˆ":"I",Ø:"O","∏":"P",Å:"A",Í:"S",Î:"D",Ï:"F","˝":"G",Ó:"H",Ô:"J","":"K",Ò:"L","¸":"Z","˛":"X",Ç:"C","◊":"V",ı:"B","˜":"N",Â:"M"};function getKey(r){return r.altKey?convertKeyTable[r.key]||(r.code.startsWith("Key")?r.code[3].toLowerCase():null)||r.key:convertKeyTable[r.key]||r.key}function convertKeyEvent(r){return modifierKeys.indexOf(r.key)!==-1?null:{key:getKey(r),ctrl:r.ctrlKey,meta:r.altKey,super:r.metaKey,shift:r.shiftKey}}var defs=[[0,31,"N"],[32,126,"Na"],[127,160,"N"],[161,161,"A"],[162,163,"Na"],[164,164,"A"],[165,166,"Na"],[167,168,"A"],[169,169,"N"],[170,170,"A"],[171,171,"N"],[172,172,"Na"],[173,174,"A"],[175,175,"Na"],[176,180,"A"],[181,181,"N"],[182,186,"A"],[187,187,"N"],[188,191,"A"],[192,197,"N"],[198,198,"A"],[199,207,"N"],[208,208,"A"],[209,214,"N"],[215,216,"A"],[217,221,"N"],[222,225,"A"],[226,229,"N"],[230,230,"A"],[231,231,"N"],[232,234,"A"],[235,235,"N"],[236,237,"A"],[238,239,"N"],[240,240,"A"],[241,241,"N"],[242,243,"A"],[244,246,"N"],[247,250,"A"],[251,251,"N"],[252,252,"A"],[253,253,"N"],[254,254,"A"],[255,256,"N"],[257,257,"A"],[258,272,"N"],[273,273,"A"],[274,274,"N"],[275,275,"A"],[276,282,"N"],[283,283,"A"],[284,293,"N"],[294,295,"A"],[296,298,"N"],[299,299,"A"],[300,304,"N"],[305,307,"A"],[308,311,"N"],[312,312,"A"],[313,318,"N"],[319,322,"A"],[323,323,"N"],[324,324,"A"],[325,327,"N"],[328,331,"A"],[332,332,"N"],[333,333,"A"],[334,337,"N"],[338,339,"A"],[340,357,"N"],[358,359,"A"],[360,362,"N"],[363,363,"A"],[364,461,"N"],[462,462,"A"],[463,463,"N"],[464,464,"A"],[465,465,"N"],[466,466,"A"],[467,467,"N"],[468,468,"A"],[469,469,"N"],[470,470,"A"],[471,471,"N"],[472,472,"A"],[473,473,"N"],[474,474,"A"],[475,475,"N"],[476,476,"A"],[477,592,"N"],[593,593,"A"],[594,608,"N"],[609,609,"A"],[610,707,"N"],[708,708,"A"],[709,710,"N"],[711,711,"A"],[712,712,"N"],[713,715,"A"],[716,716,"N"],[717,717,"A"],[718,719,"N"],[720,720,"A"],[721,727,"N"],[728,731,"A"],[732,732,"N"],[733,733,"A"],[734,734,"N"],[735,735,"A"],[736,767,"N"],[768,879,"A"],[880,912,"N"],[913,929,"A"],[930,930,"N"],[931,937,"A"],[938,944,"N"],[945,961,"A"],[962,962,"N"],[963,969,"A"],[970,1024,"N"],[1025,1025,"A"],[1026,1039,"N"],[1040,1103,"A"],[1104,1104,"N"],[1105,1105,"A"],[1106,4351,"N"],[4352,4447,"W"],[4448,8207,"N"],[8208,8208,"A"],[8209,8210,"N"],[8211,8214,"A"],[8215,8215,"N"],[8216,8217,"A"],[8218,8219,"N"],[8220,8221,"A"],[8222,8223,"N"],[8224,8226,"A"],[8227,8227,"N"],[8228,8231,"A"],[8232,8239,"N"],[8240,8240,"A"],[8241,8241,"N"],[8242,8243,"A"],[8244,8244,"N"],[8245,8245,"A"],[8246,8250,"N"],[8251,8251,"A"],[8252,8253,"N"],[8254,8254,"A"],[8255,8307,"N"],[8308,8308,"A"],[8309,8318,"N"],[8319,8319,"A"],[8320,8320,"N"],[8321,8324,"A"],[8325,8360,"N"],[8361,8361,"H"],[8362,8363,"N"],[8364,8364,"A"],[8365,8450,"N"],[8451,8451,"A"],[8452,8452,"N"],[8453,8453,"A"],[8454,8456,"N"],[8457,8457,"A"],[8458,8466,"N"],[8467,8467,"A"],[8468,8469,"N"],[8470,8470,"A"],[8471,8480,"N"],[8481,8482,"A"],[8483,8485,"N"],[8486,8486,"A"],[8487,8490,"N"],[8491,8491,"A"],[8492,8530,"N"],[8531,8532,"A"],[8533,8538,"N"],[8539,8542,"A"],[8543,8543,"N"],[8544,8555,"A"],[8556,8559,"N"],[8560,8569,"A"],[8570,8584,"N"],[8585,8585,"A"],[8586,8591,"N"],[8592,8601,"A"],[8602,8631,"N"],[8632,8633,"A"],[8634,8657,"N"],[8658,8658,"A"],[8659,8659,"N"],[8660,8660,"A"],[8661,8678,"N"],[8679,8679,"A"],[8680,8703,"N"],[8704,8704,"A"],[8705,8705,"N"],[8706,8707,"A"],[8708,8710,"N"],[8711,8712,"A"],[8713,8714,"N"],[8715,8715,"A"],[8716,8718,"N"],[8719,8719,"A"],[8720,8720,"N"],[8721,8721,"A"],[8722,8724,"N"],[8725,8725,"A"],[8726,8729,"N"],[8730,8730,"A"],[8731,8732,"N"],[8733,8736,"A"],[8737,8738,"N"],[8739,8739,"A"],[8740,8740,"N"],[8741,8741,"A"],[8742,8742,"N"],[8743,8748,"A"],[8749,8749,"N"],[8750,8750,"A"],[8751,8755,"N"],[8756,8759,"A"],[8760,8763,"N"],[8764,8765,"A"],[8766,8775,"N"],[8776,8776,"A"],[8777,8779,"N"],[8780,8780,"A"],[8781,8785,"N"],[8786,8786,"A"],[8787,8799,"N"],[8800,8801,"A"],[8802,8803,"N"],[8804,8807,"A"],[8808,8809,"N"],[8810,8811,"A"],[8812,8813,"N"],[8814,8815,"A"],[8816,8833,"N"],[8834,8835,"A"],[8836,8837,"N"],[8838,8839,"A"],[8840,8852,"N"],[8853,8853,"A"],[8854,8856,"N"],[8857,8857,"A"],[8858,8868,"N"],[8869,8869,"A"],[8870,8894,"N"],[8895,8895,"A"],[8896,8977,"N"],[8978,8978,"A"],[8979,8985,"N"],[8986,8987,"W"],[8988,9e3,"N"],[9001,9002,"W"],[9003,9192,"N"],[9193,9196,"W"],[9197,9199,"N"],[9200,9200,"W"],[9201,9202,"N"],[9203,9203,"W"],[9204,9311,"N"],[9312,9449,"A"],[9450,9450,"N"],[9451,9547,"A"],[9548,9551,"N"],[9552,9587,"A"],[9588,9599,"N"],[9600,9615,"A"],[9616,9617,"N"],[9618,9621,"A"],[9622,9631,"N"],[9632,9633,"A"],[9634,9634,"N"],[9635,9641,"A"],[9642,9649,"N"],[9650,9651,"A"],[9652,9653,"N"],[9654,9655,"A"],[9656,9659,"N"],[9660,9661,"A"],[9662,9663,"N"],[9664,9665,"A"],[9666,9669,"N"],[9670,9672,"A"],[9673,9674,"N"],[9675,9675,"A"],[9676,9677,"N"],[9678,9681,"A"],[9682,9697,"N"],[9698,9701,"A"],[9702,9710,"N"],[9711,9711,"A"],[9712,9724,"N"],[9725,9726,"W"],[9727,9732,"N"],[9733,9734,"A"],[9735,9736,"N"],[9737,9737,"A"],[9738,9741,"N"],[9742,9743,"A"],[9744,9747,"N"],[9748,9749,"W"],[9750,9755,"N"],[9756,9756,"A"],[9757,9757,"N"],[9758,9758,"A"],[9759,9791,"N"],[9792,9792,"A"],[9793,9793,"N"],[9794,9794,"A"],[9795,9799,"N"],[9800,9811,"W"],[9812,9823,"N"],[9824,9825,"A"],[9826,9826,"N"],[9827,9829,"A"],[9830,9830,"N"],[9831,9834,"A"],[9835,9835,"N"],[9836,9837,"A"],[9838,9838,"N"],[9839,9839,"A"],[9840,9854,"N"],[9855,9855,"W"],[9856,9874,"N"],[9875,9875,"W"],[9876,9885,"N"],[9886,9887,"A"],[9888,9888,"N"],[9889,9889,"W"],[9890,9897,"N"],[9898,9899,"W"],[9900,9916,"N"],[9917,9918,"W"],[9919,9919,"A"],[9920,9923,"N"],[9924,9925,"W"],[9926,9933,"A"],[9934,9934,"W"],[9935,9939,"A"],[9940,9940,"W"],[9941,9953,"A"],[9954,9954,"N"],[9955,9955,"A"],[9956,9959,"N"],[9960,9961,"A"],[9962,9962,"W"],[9963,9969,"A"],[9970,9971,"W"],[9972,9972,"A"],[9973,9973,"W"],[9974,9977,"A"],[9978,9978,"W"],[9979,9980,"A"],[9981,9981,"W"],[9982,9983,"A"],[9984,9988,"N"],[9989,9989,"W"],[9990,9993,"N"],[9994,9995,"W"],[9996,10023,"N"],[10024,10024,"W"],[10025,10044,"N"],[10045,10045,"A"],[10046,10059,"N"],[10060,10060,"W"],[10061,10061,"N"],[10062,10062,"W"],[10063,10066,"N"],[10067,10069,"W"],[10070,10070,"N"],[10071,10071,"W"],[10072,10101,"N"],[10102,10111,"A"],[10112,10132,"N"],[10133,10135,"W"],[10136,10159,"N"],[10160,10160,"W"],[10161,10174,"N"],[10175,10175,"W"],[10176,10213,"N"],[10214,10221,"Na"],[10222,10628,"N"],[10629,10630,"Na"],[10631,11034,"N"],[11035,11036,"W"],[11037,11087,"N"],[11088,11088,"W"],[11089,11092,"N"],[11093,11093,"W"],[11094,11097,"A"],[11098,11903,"N"],[11904,11929,"W"],[11930,11930,"N"],[11931,12019,"W"],[12020,12031,"N"],[12032,12245,"W"],[12246,12271,"N"],[12272,12287,"W"],[12288,12288,"F"],[12289,12350,"W"],[12351,12352,"N"],[12353,12438,"W"],[12439,12440,"N"],[12441,12543,"W"],[12544,12548,"N"],[12549,12591,"W"],[12592,12592,"N"],[12593,12686,"W"],[12687,12687,"N"],[12688,12771,"W"],[12772,12782,"N"],[12783,12830,"W"],[12831,12831,"N"],[12832,12871,"W"],[12872,12879,"A"],[12880,19903,"W"],[19904,19967,"N"],[19968,42124,"W"],[42125,42127,"N"],[42128,42182,"W"],[42183,43359,"N"],[43360,43388,"W"],[43389,44031,"N"],[44032,55203,"W"],[55204,57343,"N"],[57344,63743,"A"],[63744,64255,"W"],[64256,65023,"N"],[65024,65039,"A"],[65040,65049,"W"],[65050,65071,"N"],[65072,65106,"W"],[65107,65107,"N"],[65108,65126,"W"],[65127,65127,"N"],[65128,65131,"W"],[65132,65280,"N"],[65281,65376,"F"],[65377,65470,"H"],[65471,65473,"N"],[65474,65479,"H"],[65480,65481,"N"],[65482,65487,"H"],[65488,65489,"N"],[65490,65495,"H"],[65496,65497,"N"],[65498,65500,"H"],[65501,65503,"N"],[65504,65510,"F"],[65511,65511,"N"],[65512,65518,"H"],[65519,65532,"N"],[65533,65533,"A"],[65534,94175,"N"],[94176,94180,"W"],[94181,94191,"N"],[94192,94193,"W"],[94194,94207,"N"],[94208,100343,"W"],[100344,100351,"N"],[100352,101589,"W"],[101590,101631,"N"],[101632,101640,"W"],[101641,110575,"N"],[110576,110579,"W"],[110580,110580,"N"],[110581,110587,"W"],[110588,110588,"N"],[110589,110590,"W"],[110591,110591,"N"],[110592,110882,"W"],[110883,110897,"N"],[110898,110898,"W"],[110899,110927,"N"],[110928,110930,"W"],[110931,110932,"N"],[110933,110933,"W"],[110934,110947,"N"],[110948,110951,"W"],[110952,110959,"N"],[110960,111355,"W"],[111356,126979,"N"],[126980,126980,"W"],[126981,127182,"N"],[127183,127183,"W"],[127184,127231,"N"],[127232,127242,"A"],[127243,127247,"N"],[127248,127277,"A"],[127278,127279,"N"],[127280,127337,"A"],[127338,127343,"N"],[127344,127373,"A"],[127374,127374,"W"],[127375,127376,"A"],[127377,127386,"W"],[127387,127404,"A"],[127405,127487,"N"],[127488,127490,"W"],[127491,127503,"N"],[127504,127547,"W"],[127548,127551,"N"],[127552,127560,"W"],[127561,127567,"N"],[127568,127569,"W"],[127570,127583,"N"],[127584,127589,"W"],[127590,127743,"N"],[127744,127776,"W"],[127777,127788,"N"],[127789,127797,"W"],[127798,127798,"N"],[127799,127868,"W"],[127869,127869,"N"],[127870,127891,"W"],[127892,127903,"N"],[127904,127946,"W"],[127947,127950,"N"],[127951,127955,"W"],[127956,127967,"N"],[127968,127984,"W"],[127985,127987,"N"],[127988,127988,"W"],[127989,127991,"N"],[127992,128062,"W"],[128063,128063,"N"],[128064,128064,"W"],[128065,128065,"N"],[128066,128252,"W"],[128253,128254,"N"],[128255,128317,"W"],[128318,128330,"N"],[128331,128334,"W"],[128335,128335,"N"],[128336,128359,"W"],[128360,128377,"N"],[128378,128378,"W"],[128379,128404,"N"],[128405,128406,"W"],[128407,128419,"N"],[128420,128420,"W"],[128421,128506,"N"],[128507,128591,"W"],[128592,128639,"N"],[128640,128709,"W"],[128710,128715,"N"],[128716,128716,"W"],[128717,128719,"N"],[128720,128722,"W"],[128723,128724,"N"],[128725,128727,"W"],[128728,128731,"N"],[128732,128735,"W"],[128736,128746,"N"],[128747,128748,"W"],[128749,128755,"N"],[128756,128764,"W"],[128765,128991,"N"],[128992,129003,"W"],[129004,129007,"N"],[129008,129008,"W"],[129009,129291,"N"],[129292,129338,"W"],[129339,129339,"N"],[129340,129349,"W"],[129350,129350,"N"],[129351,129535,"W"],[129536,129647,"N"],[129648,129660,"W"],[129661,129663,"N"],[129664,129672,"W"],[129673,129679,"N"],[129680,129725,"W"],[129726,129726,"N"],[129727,129733,"W"],[129734,129741,"N"],[129742,129755,"W"],[129756,129759,"N"],[129760,129768,"W"],[129769,129775,"N"],[129776,129784,"W"],[129785,131071,"N"],[131072,196605,"W"],[196606,196607,"N"],[196608,262141,"W"],[262142,917759,"N"],[917760,917999,"A"],[918e3,983039,"N"],[983040,1048573,"A"],[1048574,1048575,"N"],[1048576,1114109,"A"],[1114110,1114111,"N"]];function getEAWOfCodePoint(r){let e=0,t=defs.length-1;for(;e!==t;){const i=e+(t-e>>1),[o,n,s]=defs[i];if(rn)e=i+1;else return s}return defs[e][2]}function getEAW(r,e=0){const t=r.codePointAt(e);if(t!==void 0)return getEAWOfCodePoint(t)}const textOffsetY=0;function isWideChar(r){switch(getEAW(r)){case"A":case"F":case"W":return!0;default:return!1}}function isMacOS(){return window.navigator.userAgent.indexOf("Mac OS X")!==-1}function computeFontSize(r){const t=document.createElement("canvas").getContext("2d");t.font=r;const i=t.measureText("W");return[Math.floor(i.width),Math.round(i.fontBoundingBoxAscent+textOffsetY+(i.emHeightDescent||0))]}function drawBlock({ctx:r,x:e,y:t,width:i,height:o,style:n}){r.fillStyle=n,r.fillRect(e,t,i,o)}function drawText({ctx:r,x:e,y:t,text:i,font:o,style:n,option:s}){t+=Math.round(textOffsetY),r.fillStyle=n,r.font=o,r.textBaseline="top";for(const l of i)isWideChar(l)?(r.fillText(l,e,t,s.fontWidth*2),e+=s.fontWidth*2):(r.fillText(l,e,t,s.fontWidth),e+=s.fontWidth)}function drawHorizontalLine({ctx:r,x:e,y:t,width:i,style:o,lineWidth:n=1}){r.strokeStyle=o,r.lineWidth=n,r.setLineDash=[],r.beginPath(),r.moveTo(e,t),r.lineTo(e+i,t),r.stroke()}class Option{constructor({fontName:e,fontSize:t}){const i=t+"px "+e;this.font=i;const[o,n]=computeFontSize(i);this.fontWidth=o,this.fontHeight=n,this.foreground="#333",this.background="#ccc"}}function getLemEditorElement(){return document.getElementById("lem-editor")}class Cursor{constructor(e,t,i){this.editor=e,this.name=t,this.color=i,this.span=document.createElement("span"),this.span.style.all="none",this.span.style.position="absolute",this.span.style.zIndex="",this.span.style.top="0",this.span.style.left="0",this.span.style.fontFamily=e.option.font,this.span.style.backgroundColor=i,this.span.style.color="white",this.span.innerHTML="",document.body.appendChild(this.span),this.timerId=null}move(e,t){const[i,o]=this.editor.getDisplayRectangle();this.span.style.visibility="visible",this.span.textContent=this.name,this.span.style.left=e+i+"px",this.span.style.top=t+o+"px",this.span.style.padding="3px 1%",this.timerId&&clearTimeout(this.timerId),this.timerId=setTimeout(()=>{this.span.style.visibility="hidden"},500)}}function addMouseEventListeners({dom:r,editor:e,isDraggable:t,draggableStyle:i}){r.addEventListener("contextmenu",s=>{s.preventDefault()});const o=(s,l)=>{s.preventDefault();const[c,a]=e.getDisplayRectangle(),u=s.clientX-c,h=s.clientY-a,N=Math.floor(u/e.option.fontWidth),d=Math.floor(h/e.option.fontHeight);e.jsonrpc.notify("input",{kind:l,value:{x:N,y:d,pixelX:u,pixelY:h,button:s.button,clicks:s.detail}})};r.addEventListener("mousedown",s=>{t&&(document.body.style.cursor=i),o(s,"mousedown")}),r.addEventListener("mouseup",s=>{t&&(document.body.style.cursor="default"),o(s,"mouseup")});let n=0;r.addEventListener("mousemove",s=>{s.preventDefault();const l=Date.now();if(l-n>50){n=l;const[c,a]=e.getDisplayRectangle(),u=s.clientX-c,h=s.clientY-a,N=Math.floor(u/e.option.fontWidth),d=Math.floor(h/e.option.fontHeight);e.jsonrpc.notify("input",{kind:"mousemove",value:{x:N,y:d,pixelX:u,pixelY:h,button:s.buttons===0?null:s.buttons-1}})}}),t&&(r.addEventListener("mouseover",()=>{document.body.style.cursor=i}),r.addEventListener("mouseout",s=>{s.buttons!==1&&(document.body.style.cursor="default")})),r.addEventListener("wheel",s=>{s.preventDefault();const[l,c]=e.getDisplayRectangle(),a=s.clientX-l,u=s.clientY-c,h=Math.floor(a/e.option.fontWidth),N=Math.floor(u/e.option.fontHeight);e.jsonrpc.notify("input",{kind:"wheel",value:{pixelX:a,pixelY:u,x:h,y:N,wheelX:-Math.round(s.deltaX*.01),wheelY:-Math.round(s.deltaY*.01)}})})}const borderOffsetX=5,borderOffsetY=10;class BaseSurface{constructor({editor:r}){this.editor=r,this.mainDOM=null,this.wrapper=null}delete(){this.wrapper?getLemEditorElement().removeChild(this.wrapper):getLemEditorElement().removeChild(this.mainDOM)}setupDOM({dom:r,isFloating:e,border:t}){this.mainDOM=r,e&&t?(this.wrapper=document.createElement("div"),this.wrapper.style.position="absolute",this.wrapper.style.padding="10px",this.wrapper.style.border="1px solid",this.wrapper.style.borderColor=this.editor.option.foreground,this.wrapper.style.backgroundColor=this.editor.option.background,this.wrapper.appendChild(r),getLemEditorElement().appendChild(this.wrapper)):getLemEditorElement().appendChild(r)}move(r,e){const[t,i]=this.editor.getDisplayRectangle(),o=Math.floor(t+r*this.editor.option.fontWidth),n=Math.floor(i+e*this.editor.option.fontHeight);this.wrapper?(this.wrapper.style.left=o-borderOffsetX+"px",this.wrapper.style.top=n-borderOffsetY+"px",this.mainDOM.style.left=borderOffsetX+"px",this.mainDOM.style.top=borderOffsetY+"px"):(this.mainDOM.style.left=o+"px",this.mainDOM.style.top=n+"px")}resize(r,e){const t=window.devicePixelRatio||1;this.mainDOM.width=r*this.editor.option.fontWidth*t,this.mainDOM.height=e*this.editor.option.fontHeight*t,this.mainDOM.style.width=r*this.editor.option.fontWidth+"px",this.mainDOM.style.height=e*this.editor.option.fontHeight+"px",this.mainDOM.getContext("2d").scale(t,t),this.wrapper&&(this.wrapper.style.width=r*this.editor.option.fontWidth+borderOffsetX*2+"px",this.wrapper.style.height=e*this.editor.option.fontHeight+borderOffsetY*2+"px")}drawBlock(r,e,t,i,o){}drawText(r,e,t,i,o){}touch(){}evalIn(code){eval(code)}}class CanvasSurface extends BaseSurface{constructor({editor:e,view:t,x:i,y:o,width:n,height:s,styles:l,isFloating:c,border:a}){super({editor:e});const u=this.setupCanvas(l);this.setupDOM({dom:u,isFloating:c,border:a}),this.move(i,o),this.resize(n,s),this.drawingQueue=[],addMouseEventListeners({dom:u,editor:e})}setupCanvas(e){const t=document.createElement("canvas");if(t.style.position="absolute",e)for(let i in e)t.style[i]=e[i];return t}drawBlock(e,t,i,o,n){const s=this.editor.option;this.drawingQueue.push(function(l){drawBlock({ctx:l,x:e*s.fontWidth,y:t*s.fontHeight,width:i*s.fontWidth,height:o*s.fontHeight,style:n})})}drawText(e,t,i,o,n){const s=this.editor.option;this.drawingQueue.push(function(l){if(!n)drawBlock({ctx:l,x:e*s.fontWidth,y:t*s.fontHeight,width:o*s.fontWidth,height:s.fontHeight,style:s.background}),drawText({ctx:l,x:e*s.fontWidth,y:t*s.fontHeight,text:i,style:s.foreground,font:s.font,option:s});else{let{foreground:c,background:a,bold:u,reverse:h,underline:N}=n;if(c||(c=s.foreground),a||(a=s.background),h){const p=a;a=c,c=p}const d=e*s.fontWidth,f=t*s.fontHeight;drawBlock({ctx:l,x:d,y:f,width:o*s.fontWidth,height:s.fontHeight,style:a}),drawText({ctx:l,x:d,y:f,text:i,style:c,font:u?"bold "+s.font:s.font,option:s}),N&&drawHorizontalLine({ctx:l,x:d,y:f+s.fontHeight-2,width:o*s.fontWidth,style:typeof N=="string"?N:c,lineWidth:2})}})}touch(){const e=this.mainDOM.getContext("2d");for(let t of this.drawingQueue)t(e);this.drawingQueue=[]}}class HTMLSurface extends BaseSurface{constructor({editor:e,x:t,y:i,width:o,height:n,styles:s,isFloating:l,border:c,html:a}){super({editor:e});const u=document.createElement("iframe");this.setupDOM({dom:u,isFloating:l,border:c}),u.style.position="absolute",u.style.backgroundColor="white",u.setAttribute("sandbox","allow-scripts allow-same-origin"),u.srcdoc=a,this.iframe=u,this.move(t,i),this.resize(o,n)}update(e){const t=this.iframe.contentWindow.scrollY;this.iframe.srcdoc=e,this.iframe.onload=()=>{this.iframe.onload=null,this.iframe.contentWindow.scrollTo(0,t)}}evalIn(e){this.iframe.contentWindow.eval(e)}}class VerticalBorder{constructor({x:e,y:t,height:i,option:o,editor:n}){this.option=o,this.editor=n,this.line=document.createElement("div"),this.line.style.backgroundColor=o.foreground,this.line.style.width="5px",this.line.style.height=i*o.fontHeight+"px",this.line.style.position="absolute",getLemEditorElement().appendChild(this.line),this.move(e,t),addMouseEventListeners({dom:this.line,editor:n,isDraggable:!0,draggableStyle:"col-resize"})}delete(){this.line.parentNode.removeChild(this.line)}move(e,t){const[i,o]=this.editor.getDisplayRectangle();this.line.style.left=Math.floor(i+e*this.option.fontWidth-this.option.fontWidth/2)+"px",this.line.style.top=o+t*this.option.fontHeight+"px"}resize(e){this.line.style.height=e*this.option.fontHeight+"px"}}const viewStyles={tile:()=>{},floating:r=>({boxSizing:"border-box",borderColor:r.foreground,backgroundColor:r.background})};function getViewStyle(r,e){return viewStyles[r](e)||{}}class View{constructor({id:e,x:t,y:i,width:o,height:n,useModeline:s,kind:l,type:c,content:a,border:u,option:h,editor:N}){switch(this.option=h,this.id=e,this.x=t,this.y=i,this.width=o,this.height=n,this.useModeline=s,this.kind=l,this.type=c,this.border=u,this.editor=N,this.leftsideBar=null,l){case"tile":this.mainSurface=this.makeSurface(c,a),this.leftSideBar=new VerticalBorder({x:t,y:i,height:n+(s?1:0),option:h,editor:N});break;case"floating":this.mainSurface=this.makeSurface(c,a);break}this.modelineSurface=s?this.makeModelineSurface():null}delete(){this.mainSurface.delete(),this.modelineSurface&&this.modelineSurface.delete(),this.leftSideBar&&this.leftSideBar.delete()}move(e,t){this.x=e,this.y=t,this.mainSurface.move(e,t),this.modelineSurface&&this.modelineSurface.move(e,t+this.height),this.leftSideBar&&this.leftSideBar.move(e,t)}resize(e,t){this.width=e,this.height=t,this.mainSurface.resize(e,t),this.modelineSurface&&(this.modelineSurface.move(this.x,this.y+this.height),this.modelineSurface.resize(e,1)),this.leftSideBar&&this.leftSideBar.resize(t+1)}clear(){this.mainSurface.drawBlock(0,0,this.width,this.height,this.option.background)}clearEol(e,t){this.mainSurface.drawBlock(e,t,this.width-e,1,this.option.background)}clearEob(e,t){this.mainSurface.drawBlock(e,t,this.width,this.height-t,this.option.background)}print(e,t,i,o,n){this.mainSurface.drawText(e,t,i,o,n)}printToModeline(e,t,i,o,n){this.modelineSurface&&this.modelineSurface.drawText(e,t,i,o,n)}touch(){this.mainSurface.touch(),this.modelineSurface&&this.modelineSurface.touch()}makeSurface(e,t){switch(e){case"html":return this.makeHTMLSurface(t);case"editor":return this.makeEditorSurface();default:console.error(`unknown type: ${e}`)}}makeHTMLSurface(e){return new HTMLSurface({editor:this.editor,x:this.x,y:this.y,width:this.width,height:this.height,styles:getViewStyle(this.kind,this.option),isFloating:this.kind==="floating",border:this.border,html:e})}makeEditorSurface(){return new CanvasSurface({option:this.editor.option,x:this.x,y:this.y,width:this.width,height:this.height,styles:getViewStyle(this.kind,this.option),editor:this.editor,border:this.border,isFloating:this.kind==="floating",view:this})}makeModelineSurface(){const e=new CanvasSurface({option:this.editor.option,x:this.x,y:this.y+this.height,width:this.width,height:1,editor:this.editor,view:this});return addMouseEventListeners({dom:e.mainDOM,editor:this.editor,isDraggable:!0,draggableStyle:"row-resize"}),e}changeToHTMLContent(e){this.mainSurface.constructor.name==="HTMLSurface"?this.mainSurface.update(e):(this.mainSurface.delete(),this.mainSurface=this.makeHTMLSurface(e))}changeToEditorContent(){this.mainSurface.delete(),this.mainSurface=this.makeEditorSurface()}evalIn(e){this.mainSurface.evalIn(e)}}function isPasteKeyEvent(r){return isMacOS()?!1:r.ctrlKey&&r.shiftKey&&r.key==="V"}class Input{constructor(e){const t=e.option;this.editor=e,this.composition=!1,this.span=document.createElement("span"),this.span.style.color=t.foreground,this.span.style.backgroundColor=t.background,this.span.style.position="absolute",this.span.style.zIndex="",this.span.style.top="0",this.span.style.left="0",this.span.style.font=t.font,this.input=document.createElement("input"),this.input.style.backgroundColor="transparent",this.input.style.color="transparent",this.input.style.width="0",this.input.style.padding="0",this.input.style.margin="0",this.input.style.border="none",this.input.style.position="absolute",this.input.style.zIndex="-10",this.input.style.top="0",this.input.style.left="0",this.input.style.font=t.font,this.input.addEventListener("blur",i=>{this.editor.inputEnabled&&this.input.focus()}),this.input.addEventListener("input",i=>{this.editor.inputEnabled&&this.composition===!1&&(this.input.value="",this.span.innerHTML="",this.input.style.width="0",this.editor.emitInputString(i.data))}),this.input.addEventListener("keydown",i=>{if(this.editor.inputEnabled){if(isPasteKeyEvent(i)){this.editor.jsonrpc.notify("input",{kind:"clipboard-paste"});return}if(i.isComposing||this.composition||i.key==="Process"||!isMacOS()&&!i.ctrlKey&&!i.altKey&&i.key.length===1)return;if(i.preventDefault(),i.isComposing!==!0&&i.code!=="")return this.editor.emitInput(i),this.input.value="",!1}}),this.input.addEventListener("compositionstart",i=>{this.editor.inputEnabled&&(this.composition=!0,this.span.innerHTML=this.input.value,this.input.style.width=this.span.offsetWidth+"px")}),this.input.addEventListener("compositionupdate",i=>{this.editor.inputEnabled&&(this.span.innerHTML=i.data,this.input.style.width=this.span.offsetWidth+"px")}),this.input.addEventListener("compositionend",i=>{this.editor.inputEnabled&&(this.composition=!1,this.editor.emitInputString(this.input.value),this.input.value="",this.span.innerHTML=this.input.value,this.input.style.width="0")}),document.body.appendChild(this.input),document.body.appendChild(this.span),this.input.focus()}finalize(){document.body.removeChild(this.input),document.body.removeChild(this.span)}move(e,t){const[i,o]=this.editor.getDisplayRectangle();this.span.style.top=o+t+"px",this.span.style.left=i+e+"px",this.input.style.top=this.span.offsetTop+"px",this.input.style.left=this.span.offsetLeft+"px"}updateForeground(e){this.span.style.color=e}updateBackground(e){this.span.style.backgroundColor=e}}class MessageTable{constructor(){this.map=new Map}register(e,t){for(const i in t){const o=t[i];this.map.set(i,o),e.on(i,o)}}get(e){return this.map.get(e)}}function getDisplayRectangleDefault(){return[0,0,window.innerWidth,window.innerHeight]}class Editor{constructor({getDisplayRectangle:e=getDisplayRectangleDefault,fontName:t,fontSize:i,onLoaded:o,url:n,onExit:s,onClosed:l,onRestart:c,onUserInput:a,onSwitchFile:u}){this.getDisplayRectangle=e,this.option=new Option({fontName:t,fontSize:i}),this.onExit=s,this.onLoaded=o,this.onRestart=c,this.onUserInput=a,this.onSwitchFile=u,this.inputEnabled=!0,this.input=new Input(this),this.cursors=new Map,this.viewMap=new Map,this.jsonrpc=new JSONRPC(n,{onClosed:()=>{l()}}),this.messageTable=new MessageTable,this.messageTable.register(this.jsonrpc,{startup:this.startup.bind(this),"update-foreground":this.updateForeground.bind(this),"update-background":this.updateBackground.bind(this),"make-view":this.makeView.bind(this),"delete-view":this.deleteView.bind(this),"resize-view":this.resize.bind(this),"move-view":this.move.bind(this),"redraw-view-after":this.redrawViewAfter.bind(this),clear:this.clear.bind(this),"clear-eol":this.clearEol.bind(this),"clear-eob":this.clearEob.bind(this),put:this.put.bind(this),"modeline-put":this.modelinePut.bind(this),"update-display":this.updateDisplay.bind(this),"move-cursor":this.moveCursor.bind(this),"change-view":this.changeView.bind(this),"resize-display":this.resizeDisplay.bind(this),bulk:this.bulk.bind(this),exit:this.exitEditor.bind(this),"user-input":this.userInput.bind(this),"switch-file":this.switchFile.bind(this),"get-clipboard-text":this.getClipboardText.bind(this),"set-clipboard-text":this.setClipboardText.bind(this),"js-eval":this.jsEval.bind(this)}),this.login(),this.boundedHandleResize=this.handleResize.bind(this)}init(){window.addEventListener("resize",this.boundedHandleResize),document.getElementsByTagName("html")[0].style["background-color"]="#333"}finalize(){window.removeEventListener("resize",this.boundedHandleResize),this.input.finalize()}closeConnection(){this.jsonrpc.close()}emitInput(e){const t=convertKeyEvent(e);if(t){if(t.key==="]"&&t.ctrl&&!t.meta&&!t.super&&!t.shift){this.jsonrpc.notify("input",{kind:"abort"});return}this.jsonrpc.notify("input",{kind:"key",value:t})}}emitInputString(e){e?this.jsonrpc.notify("input",{kind:"input-string",value:e}):console.error("unexpected argument",e)}handleResize(e){this.jsonrpc.notify("redraw",{size:this.getDisplaySize()})}enableInput(){this.inputEnabled=!0}disableInput(){this.inputEnabled=!1}sendNotification(e,t){this.jsonrpc.notify(e,t)}request(e,t,i){this.jsonrpc.request(e,t,i)}getDisplaySize(){const[e,t,i,o]=this.getDisplayRectangle(),n=Math.round(i/this.option.fontWidth),s=Math.round(o/this.option.fontHeight);return{width:n,height:s}}callMessage(e,t){this.messageTable.get(e)(t)}findViewById(e){return this.viewMap.get(e)}login(){this.jsonrpc.request("login",{size:this.getDisplaySize(),foreground:this.option.foreground,background:this.option.background},e=>{if(this.updateForeground(e.foreground),this.updateBackground(e.background),e.views)for(const t of e.views)this.makeView(t);this.jsonrpc.notify("redraw",{size:this.getDisplaySize()}),this.jsonrpc.request("user-file-map",{},t=>{this.onSwitchFile(t)})})}startup(){this.onRestart&&this.onRestart()}updateForeground(e){this.option.foreground=e,this.input.updateForeground(e)}updateBackground(e){this.option.background=e,this.input.updateBackground(e);const t=getLemEditorElement();t.style.backgroundColor=e}makeView({id:e,x:t,y:i,width:o,height:n,use_modeline:s,kind:l,type:c,content:a,border:u}){const h=new View({option:this.option,id:e,x:t,y:i,width:o,height:n,useModeline:s,kind:l,type:c,content:a,border:u,editor:this});this.viewMap.set(e,h)}deleteView({viewInfo:{id:e}}){this.findViewById(e).delete(),this.viewMap.delete(e)}resize({viewInfo:{id:e},width:t,height:i}){this.findViewById(e).resize(t,i)}move({viewInfo:{id:e},x:t,y:i}){this.findViewById(e).move(t,i)}redrawViewAfter({viewInfo:{id:e},html:t}){this.findViewById(e).touch()}clear({viewInfo:{id:e}}){this.findViewById(e).clear()}clearEol({viewInfo:{id:e},x:t,y:i}){this.findViewById(e).clearEol(t,i)}clearEob({viewInfo:{id:e},x:t,y:i}){this.findViewById(e).clearEob(t,i)}put({viewInfo:{id:e},x:t,y:i,text:o,textWidth:n,attribute:s,cursorInfo:l}){const c=this.findViewById(e);if(c.print(t,i,o,n,s),l){const{name:a,color:u}=l;let h=this.cursors.get(a);h||(h=new Cursor(this,a,u),this.cursors.set(a,h)),h.move((c.x+t)*this.option.fontWidth,(c.y+i-1)*this.option.fontHeight)}}modelinePut({viewInfo:{id:e},x:t,y:i,text:o,textWidth:n,attribute:s}){this.findViewById(e).printToModeline(t,i,o,n,s)}updateDisplay(){}moveCursor({viewInfo:{id:e},x:t,y:i}){const o=this.findViewById(e),n=o.x*this.option.fontWidth+t*this.option.fontWidth,s=o.y*this.option.fontHeight+i*this.option.fontHeight;this.input.move(n,s)}changeView({viewInfo:{id:e},type:t,content:i}){const o=this.findViewById(e);switch(t){case"html":o.changeToHTMLContent(i);break;case"editor":o.changeToEditorContent();break}}resizeDisplay({width:e,height:t}){const i=getLemEditorElement();i.style.width=Math.floor(e*this.option.fontWidth)+"px",i.style.height=Math.floor(t*this.option.fontHeight)+"px"}bulk(e){this.onLoaded&&(this.onLoaded(),this.onLoaded=null);for(const{method:t,argument:i}of e)this.callMessage(t,i)}exitEditor(){this.onExit&&this.onExit()}userInput({value:e}){this.onUserInput&&this.onUserInput(e)}switchFile(e){this.onSwitchFile&&this.onSwitchFile(e)}getClipboardText(){navigator.clipboard.readText().then(e=>{this.jsonrpc.notify("got-clipboard-text",{text:e})})}setClipboardText({text:e}){navigator.clipboard&&navigator.clipboard.writeText(e)}jsEval({viewInfo:{id:e},code:t}){this.findViewById(e).evalIn(t)}}const canvas=document.querySelector("#editor");function main(){document.fonts.ready.then(()=>{new Editor({canvas,fontName:"Monospace",fontSize:19,onLoaded:null,url:`ws://${window.location.hostname}:50000`,onExit:null,onClosed:null,onRestart:null,onUserInput:null}).init()})}main(); diff --git a/frontends/server/frontend/dist/index.html b/frontends/server/frontend/dist/index.html new file mode 100644 index 000000000..32653e0f8 --- /dev/null +++ b/frontends/server/frontend/dist/index.html @@ -0,0 +1,23 @@ + + + + + + Lem + + + + + + +
+ + diff --git a/frontends/server/frontend/editor.js b/frontends/server/frontend/editor.js new file mode 100644 index 000000000..68cff2c91 --- /dev/null +++ b/frontends/server/frontend/editor.js @@ -0,0 +1,1210 @@ +"use strict"; + +import { JSONRPC } from './jsonrpc.js'; +import * as keyevent from './keyevent.js'; +import * as meaw from 'meaw'; + +const textOffsetY = 0; + +function isWideChar(c) { + switch (meaw.getEAW(c)) { + case 'A': + case 'F': + case 'W': + return true; + default: + return false; + } +} + +function isMacOS() { + return window.navigator.userAgent.indexOf('Mac OS X') !== -1; +} + +function computeFontSize(font) { + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + ctx.font = font; + + const textMetrics = ctx.measureText('W'); + + return [ + Math.floor(textMetrics.width), + Math.round(textMetrics.fontBoundingBoxAscent + textOffsetY + (textMetrics.emHeightDescent || 0)), + ]; +} + +function drawBlock({ ctx, x, y, width, height, style }) { + ctx.fillStyle = style; + ctx.fillRect(x, y, width, height); +} + +function drawText({ ctx, x, y, text, font, style, option }) { + y += Math.round(textOffsetY); // 少しずらしておかないと上の部分が現在行からはみ出して、その行だけ再描画しても描画跡が残ってしまう + ctx.fillStyle = style; + ctx.font = font; + ctx.textBaseline = 'top'; + + for (const c of text) { + if (isWideChar(c)) { + ctx.fillText(c, x, y, option.fontWidth * 2); + x += option.fontWidth * 2; + } else { + ctx.fillText(c, x, y, option.fontWidth); + x += option.fontWidth; + } + } +} + +function drawHorizontalLine({ ctx, x, y, width, style, lineWidth = 1 }) { + ctx.strokeStyle = style; + ctx.lineWidth = lineWidth; + ctx.setLineDash = []; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + width, y); + ctx.stroke(); +} + +class Option { + constructor({ fontName, fontSize }) { + const font = fontSize + 'px ' + fontName; + this.font = font; + const [width, height] = computeFontSize(font); + this.fontWidth = width; + this.fontHeight = height; + this.foreground = '#333'; + this.background = '#ccc'; + } +} + +function getLemEditorElement() { + return document.getElementById('lem-editor'); +} + +class Cursor { + constructor(editor, name, color) { + this.editor = editor; + this.name = name; + this.color = color; + + this.span = document.createElement('span'); + this.span.style.all = 'none'; + this.span.style.position = 'absolute'; + this.span.style.zIndex = ''; + this.span.style.top = '0'; + this.span.style.left = '0'; + this.span.style.fontFamily = editor.option.font; + this.span.style.backgroundColor = color; + this.span.style.color = 'white'; + this.span.innerHTML = ''; + document.body.appendChild(this.span); + + this.timerId = null; + } + + move(x, y) { + const [x0, y0] = this.editor.getDisplayRectangle(); + this.span.style.visibility = 'visible'; + this.span.textContent = this.name; + this.span.style.left = (x + x0) + 'px'; + this.span.style.top = (y + y0) + 'px'; + this.span.style.padding = '3px 1%' + if (this.timerId) { + clearTimeout(this.timerId); + } + this.timerId = setTimeout( + () => { + this.span.style.visibility = 'hidden'; + }, + 500, + ); + } +} + +function addMouseEventListeners({dom, editor, isDraggable, draggableStyle}) { + dom.addEventListener('contextmenu', (event) => { + event.preventDefault(); + }); + + const handleMouseDownUp = (event, eventName) => { + event.preventDefault(); + const [displayX, displayY] = editor.getDisplayRectangle(); + + const pixelX = (event.clientX - displayX); + const pixelY = (event.clientY - displayY); + const x = Math.floor(pixelX / editor.option.fontWidth); + const y = Math.floor(pixelY / editor.option.fontHeight); + + editor.jsonrpc.notify('input', { + kind: eventName, + value: { + x: x, + y: y, + pixelX: pixelX, + pixelY: pixelY, + button: event.button, + clicks: event.detail, + } + }); + }; + + dom.addEventListener('mousedown', (event) => { + if (isDraggable) document.body.style.cursor = draggableStyle; + handleMouseDownUp(event, 'mousedown'); + }); + + dom.addEventListener('mouseup', (event) => { + if (isDraggable) document.body.style.cursor = 'default'; + handleMouseDownUp(event, 'mouseup'); + }); + + let lastMouseMoveTime = 0; + dom.addEventListener('mousemove', (event) => { + event.preventDefault(); + const now = Date.now(); + if (now - lastMouseMoveTime > 50) { + lastMouseMoveTime = now; + const [displayX, displayY] = editor.getDisplayRectangle(); + + const pixelX = (event.clientX - displayX); + const pixelY = (event.clientY - displayY); + const x = Math.floor(pixelX / editor.option.fontWidth); + const y = Math.floor(pixelY / editor.option.fontHeight); + editor.jsonrpc.notify('input', { + kind: 'mousemove', + value: { + x: x, + y: y, + pixelX: pixelX, + pixelY: pixelY, + button: event.buttons === 0 ? null : event.buttons - 1, + } + }); + } + }); + + if (isDraggable) { + dom.addEventListener('mouseover', () => { + document.body.style.cursor = draggableStyle; + }); + dom.addEventListener('mouseout', (e) => { + if (e.buttons !== 1) { + document.body.style.cursor = 'default'; + } + }); + } + + dom.addEventListener('wheel', (event) => { + event.preventDefault(); + const [displayX, displayY] = editor.getDisplayRectangle(); + const pixelX = (event.clientX - displayX); + const pixelY = (event.clientY - displayY); + const x = Math.floor(pixelX / editor.option.fontWidth); + const y = Math.floor(pixelY / editor.option.fontHeight); + + editor.jsonrpc.notify('input', { + kind: 'wheel', + value: { + pixelX: pixelX, + pixelY: pixelY, + x: x, + y: y, + wheelX: -Math.round(event.deltaX * 0.01), + wheelY: -Math.round(event.deltaY * 0.01), + }, + }); + }); +} + +const borderOffsetX = 5; +const borderOffsetY = 10; + +class BaseSurface { + constructor({ editor }) { + this.editor = editor; + this.mainDOM = null; + this.wrapper = null; + } + + delete() { + if (this.wrapper) { + getLemEditorElement().removeChild(this.wrapper); + } else { + getLemEditorElement().removeChild(this.mainDOM); + } + } + + setupDOM({ dom, isFloating, border }) { + this.mainDOM = dom; + + if (isFloating && border) { + this.wrapper = document.createElement('div'); + this.wrapper.style.position = 'absolute'; + this.wrapper.style.padding = '10px'; + this.wrapper.style.border = '1px solid'; + this.wrapper.style.borderColor = this.editor.option.foreground; + this.wrapper.style.backgroundColor = this.editor.option.background; + this.wrapper.appendChild(dom); + getLemEditorElement().appendChild(this.wrapper); + } else { + getLemEditorElement().appendChild(dom); + } + } + + move(x, y) { + const [x0, y0] = this.editor.getDisplayRectangle(); + const left = Math.floor(x0 + x * this.editor.option.fontWidth); + const top = Math.floor(y0 + y * this.editor.option.fontHeight); + if (this.wrapper) { + this.wrapper.style.left = left - borderOffsetX + 'px'; + this.wrapper.style.top = top - borderOffsetY + 'px'; + this.mainDOM.style.left = borderOffsetX + 'px'; + this.mainDOM.style.top = borderOffsetY + 'px'; + } else { + this.mainDOM.style.left = left + 'px'; + this.mainDOM.style.top = top + 'px'; + } + } + + resize(width, height) { + const ratio = window.devicePixelRatio || 1; + this.mainDOM.width = width * this.editor.option.fontWidth * ratio; + this.mainDOM.height = height * this.editor.option.fontHeight * ratio; + this.mainDOM.style.width = width * this.editor.option.fontWidth + 'px'; + this.mainDOM.style.height = height * this.editor.option.fontHeight + 'px'; + + const ctx = this.mainDOM.getContext('2d'); + ctx.scale(ratio, ratio); + + if (this.wrapper) { + this.wrapper.style.width = width * this.editor.option.fontWidth + borderOffsetX * 2 + 'px'; + this.wrapper.style.height = height * this.editor.option.fontHeight + borderOffsetY * 2 + 'px'; + } + } + + drawBlock(x, y, width, height, color) { } + drawText(x, y, text, textWidth, attribute) { } + + touch() { + return; + } + + evalIn(code) { + eval(code); + } +} + +class CanvasSurface extends BaseSurface { + constructor({ editor, view, x, y, width, height, styles, isFloating, border }) { + super({ editor }); + + const canvas = this.setupCanvas(styles); + this.setupDOM({ dom: canvas, isFloating, border }); + this.move(x, y); + this.resize(width, height); + + this.drawingQueue = []; + + addMouseEventListeners({dom:canvas, editor}); + } + + setupCanvas(styles) { + const canvas = document.createElement('canvas'); + canvas.style.position = 'absolute'; + if (styles) { + for (let key in styles) { + canvas.style[key] = styles[key]; + } + } + return canvas; + } + + drawBlock(x, y, width, height, color) { + const option = this.editor.option; + this.drawingQueue.push(function(ctx) { + drawBlock({ + ctx, + x: x * option.fontWidth, + y: y * option.fontHeight, + width: width * option.fontWidth, + height: height * option.fontHeight, + style: color, + }) + }); + } + + drawText(x, y, text, textWidth, attribute) { + const option = this.editor.option; + this.drawingQueue.push(function(ctx) { + if (!attribute) { + drawBlock({ + ctx, + x: x * option.fontWidth, + y: y * option.fontHeight, + width: textWidth * option.fontWidth, + height: option.fontHeight, + style: option.background, + }); + drawText({ + ctx, + x: x * option.fontWidth, + y: y * option.fontHeight, + text: text, + style: option.foreground, + font: option.font, + option, + }); + } else { + let { foreground, background, bold, reverse, underline } = attribute; + if (!foreground) { + foreground = option.foreground; + } + if (!background) { + background = option.background; + } + if (reverse) { + const tmp = background; + background = foreground; + foreground = tmp; + } + const gx = x * option.fontWidth; + const gy = y * option.fontHeight; + drawBlock({ + ctx, + x: gx, + y: gy, + width: textWidth * option.fontWidth, + height: option.fontHeight, + style: background, + }); + drawText({ + ctx, + x: gx, + y: gy, + text: text, + style: foreground, + font: bold ? ('bold ' + option.font) : option.font, + option, + }); + if (underline) { + drawHorizontalLine({ + ctx, + x: gx, + y: gy + option.fontHeight - 2, + width: textWidth * option.fontWidth, + style: typeof (underline) === 'string' ? underline : foreground, + lineWidth: 2 + }); + } + } + }); + } + + touch() { + const ctx = this.mainDOM.getContext('2d'); + for (let fn of this.drawingQueue) { + fn(ctx); + } + this.drawingQueue = []; + } +} + +class HTMLSurface extends BaseSurface { + constructor({ editor, x, y, width, height, styles, isFloating, border, html }) { + super({ editor }); + + const iframe = document.createElement('iframe'); + this.setupDOM({ dom: iframe, isFloating, border }); + iframe.style.position = 'absolute'; + iframe.style.backgroundColor = 'white'; + iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin'); + iframe.srcdoc = html; + + this.iframe = iframe; + + this.move(x, y); + this.resize(width, height); + } + + update(content) { + const scrollY = this.iframe.contentWindow.scrollY; + this.iframe.srcdoc = content; + this.iframe.onload = () => { + this.iframe.onload = null; + this.iframe.contentWindow.scrollTo(0, scrollY); + }; + } + + evalIn(code) { + this.iframe.contentWindow.eval(code); + } +} + +class VerticalBorder { + constructor({ x, y, height, option, editor }) { + this.option = option; + this.editor = editor; + this.line = document.createElement('div'); + this.line.style.backgroundColor = option.foreground; + this.line.style.width = '5px'; + this.line.style.height = height * option.fontHeight + 'px'; + this.line.style.position = 'absolute'; + + getLemEditorElement().appendChild(this.line); + + this.move(x, y); + + addMouseEventListeners({dom:this.line, editor, isDraggable: true, draggableStyle: 'col-resize'}); + } + + delete() { + this.line.parentNode.removeChild(this.line); + } + + move(x, y) { + const [x0, y0] = this.editor.getDisplayRectangle(); + this.line.style.left = Math.floor(x0 + x * this.option.fontWidth - this.option.fontWidth / 2) + 'px'; + this.line.style.top = (y0 + y * this.option.fontHeight) + 'px'; + } + + resize(height) { + this.line.style.height = height * this.option.fontHeight + 'px'; + } +} + +const viewStyles = { + tile: () => { }, + floating: (option) => ({ + boxSizing: 'border-box', + borderColor: option.foreground, + backgroundColor: option.background, + }), +}; + +function getViewStyle(kind, option) { + return viewStyles[kind](option) || {}; +} + +class View { + constructor({ + id, + x, + y, + width, + height, + useModeline, + kind, + type, + content, + border, + option, + editor, + }) { + this.option = option; + this.id = id; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.useModeline = useModeline; + this.kind = kind; + this.type = type; + this.border = border; + this.editor = editor; + + this.leftsideBar = null; + + switch (kind) { + case 'tile': + this.mainSurface = this.makeSurface(type, content); + this.leftSideBar = new VerticalBorder({ + x: x, + y: y, + height: height + (useModeline ? 1 : 0), + option: option, + editor: editor, + }); + break; + case 'floating': + this.mainSurface = this.makeSurface(type, content); + break; + } + + this.modelineSurface = useModeline ? this.makeModelineSurface() : null; + } + + delete() { + this.mainSurface.delete(); + if (this.modelineSurface) { + this.modelineSurface.delete(); + } + if (this.leftSideBar) { + this.leftSideBar.delete(); + } + } + + move(x, y) { + this.x = x; + this.y = y; + + this.mainSurface.move(x, y); + if (this.modelineSurface) { + this.modelineSurface.move(x, y + this.height); + } + if (this.leftSideBar) { + this.leftSideBar.move(x, y); + } + } + + resize(width, height) { + this.width = width; + this.height = height; + this.mainSurface.resize(width, height); + if (this.modelineSurface) { + this.modelineSurface.move( + this.x, + this.y + this.height, + ); + this.modelineSurface.resize(width, 1); + } + if (this.leftSideBar) { + this.leftSideBar.resize(height + 1); + } + } + + clear() { + this.mainSurface.drawBlock( + 0, + 0, + this.width, + this.height, + this.option.background, + ); + } + + clearEol(x, y) { + this.mainSurface.drawBlock( + x, + y, + this.width - x, + 1, + this.option.background, + ); + } + + clearEob(x, y) { + this.mainSurface.drawBlock( + x, // x === 0 + y, + this.width, + this.height - y, + this.option.background, + ); + } + + print(x, y, text, textWidth, attribute) { + this.mainSurface.drawText( + x, + y, + text, + textWidth, + attribute, + ); + } + + printToModeline(x, y, text, textWidth, attribute) { + if (this.modelineSurface) { + this.modelineSurface.drawText( + x, + y, + text, + textWidth, + attribute, + ); + } + } + + touch() { + this.mainSurface.touch(); + if (this.modelineSurface) { + this.modelineSurface.touch(); + } + } + + makeSurface(type, content) { + switch (type) { + case 'html': + return this.makeHTMLSurface(content); + case 'editor': + return this.makeEditorSurface(); + default: + console.error(`unknown type: ${type}`); + } + } + + makeHTMLSurface(content) { + return new HTMLSurface({ + editor: this.editor, + x: this.x, + y: this.y, + width: this.width, + height: this.height, + styles: getViewStyle(this.kind, this.option), + isFloating: this.kind === 'floating', + border: this.border, + html: content, + }); + } + + makeEditorSurface() { + return new CanvasSurface({ + option: this.editor.option, + x: this.x, + y: this.y, + width: this.width, + height: this.height, + styles: getViewStyle(this.kind, this.option), + editor: this.editor, + border: this.border, + isFloating: this.kind === 'floating', + view: this, + }) + } + + makeModelineSurface() { + const surface = new CanvasSurface({ + option: this.editor.option, + x: this.x, + y: this.y + this.height, + width: this.width, + height: 1, + editor: this.editor, + view: this, + }); + addMouseEventListeners({ + dom: surface.mainDOM, + editor: this.editor, + isDraggable: true, + draggableStyle: 'row-resize', + }); + return surface; + } + + changeToHTMLContent(content) { + if (this.mainSurface.constructor.name === 'HTMLSurface') { + this.mainSurface.update(content); + } else { + this.mainSurface.delete(); + this.mainSurface = this.makeHTMLSurface(content); + } + } + + changeToEditorContent() { + this.mainSurface.delete(); + this.mainSurface = this.makeEditorSurface(); + } + + evalIn(code) { + this.mainSurface.evalIn(code); + } +} + +function isPasteKeyEvent(event) { + if (isMacOS()) { + // TODO + return false; + } else { + return (event.ctrlKey && event.shiftKey && event.key === 'V'); + } +} + +class Input { + constructor(editor) { + const option = editor.option; + this.editor = editor; + + this.composition = false; + + this.span = document.createElement('span'); + this.span.style.color = option.foreground; + this.span.style.backgroundColor = option.background; + this.span.style.position = 'absolute'; + this.span.style.zIndex = ''; + this.span.style.top = '0'; + this.span.style.left = '0'; + this.span.style.font = option.font; + + this.input = document.createElement('input'); + this.input.style.backgroundColor = 'transparent'; + this.input.style.color = 'transparent'; + this.input.style.width = '0'; + this.input.style.padding = '0'; + this.input.style.margin = '0'; + this.input.style.border = 'none'; + this.input.style.position = 'absolute'; + this.input.style.zIndex = '-10'; + this.input.style.top = '0'; + this.input.style.left = '0'; + this.input.style.font = option.font; + + this.input.addEventListener('blur', (event) => { + if (this.editor.inputEnabled) { + this.input.focus(); + } + }); + + this.input.addEventListener('input', (event) => { + if (this.editor.inputEnabled) { + if (this.composition === false) { + this.input.value = ''; + this.span.innerHTML = ''; + this.input.style.width = '0'; + this.editor.emitInputString(event.data); + } + } + }); + + this.input.addEventListener('keydown', (event) => { + if (this.editor.inputEnabled) { + + if (isPasteKeyEvent(event)) { + this.editor.jsonrpc.notify('input', { kind: 'clipboard-paste' }); + return; + } + + if (event.isComposing || this.composition) { + return; + } + + // IMEに変換を指示する値はlemに渡すと誤作動するのでreturnする + if (event.key === 'Process') { + return; + } + + if (!isMacOS()) { + // 修飾キーなしで、ReturnやBackspaceではなく'a'などの入力であるか(event.key.length === 1) + if (!event.ctrlKey && !event.altKey && event.key.length === 1) { + // そうであれば'input' eventが受け取れるようにここでreturnする + return; + } + } + + event.preventDefault(); + + if (event.isComposing !== true && event.code !== '') { + this.editor.emitInput(event); + this.input.value = ''; + return false; + } + } + }); + + this.input.addEventListener('compositionstart', (event) => { + if (this.editor.inputEnabled) { + this.composition = true; + this.span.innerHTML = this.input.value; + this.input.style.width = this.span.offsetWidth + 'px'; + } + }); + + this.input.addEventListener('compositionupdate', (event) => { + if (this.editor.inputEnabled) { + this.span.innerHTML = event.data; + this.input.style.width = this.span.offsetWidth + 'px'; + } + }); + + this.input.addEventListener('compositionend', (event) => { + if (this.editor.inputEnabled) { + this.composition = false; + this.editor.emitInputString(this.input.value); + this.input.value = ''; + this.span.innerHTML = this.input.value; + this.input.style.width = '0'; + } + }); + + document.body.appendChild(this.input); + document.body.appendChild(this.span); + this.input.focus(); + } + + finalize() { + document.body.removeChild(this.input); + document.body.removeChild(this.span); + } + + move(left, top) { + const [x0, y0] = this.editor.getDisplayRectangle(); + this.span.style.top = (y0 + top) + 'px'; + this.span.style.left = (x0 + left) + 'px'; + this.input.style.top = this.span.offsetTop + 'px'; + this.input.style.left = this.span.offsetLeft + 'px'; + } + + updateForeground(color) { + this.span.style.color = color; + } + + updateBackground(color) { + this.span.style.backgroundColor = color; + } +} + +class MessageTable { + constructor() { + this.map = new Map(); + } + + register(jsonrpc, table) { + for (const method in table) { + const handler = table[method]; + this.map.set(method, handler); + jsonrpc.on(method, handler); + } + } + + get(method) { + return this.map.get(method); + } +} + +function getDisplayRectangleDefault() { + return [0, 0, window.innerWidth, window.innerHeight]; +} + +export class Editor { + constructor({ + getDisplayRectangle = getDisplayRectangleDefault, + fontName, + fontSize, + onLoaded, + url, + onExit, + onClosed, + onRestart, + onUserInput, + onSwitchFile, + }) { + this.getDisplayRectangle = getDisplayRectangle; + + this.option = new Option({ fontName, fontSize }); + + this.onExit = onExit; + this.onLoaded = onLoaded; + this.onRestart = onRestart; + this.onUserInput = onUserInput; + this.onSwitchFile = onSwitchFile; + this.inputEnabled = true; + + this.input = new Input(this); + this.cursors = new Map(); + + this.viewMap = new Map(); + + this.jsonrpc = new JSONRPC(url, { + onClosed: () => { + onClosed(); + }, + }); + + this.messageTable = new MessageTable(); + this.messageTable.register(this.jsonrpc, { + 'startup': this.startup.bind(this), + 'update-foreground': this.updateForeground.bind(this), + 'update-background': this.updateBackground.bind(this), + 'make-view': this.makeView.bind(this), + 'delete-view': this.deleteView.bind(this), + 'resize-view': this.resize.bind(this), + 'move-view': this.move.bind(this), + 'redraw-view-after': this.redrawViewAfter.bind(this), + 'clear': this.clear.bind(this), + 'clear-eol': this.clearEol.bind(this), + 'clear-eob': this.clearEob.bind(this), + 'put': this.put.bind(this), + 'modeline-put': this.modelinePut.bind(this), + 'update-display': this.updateDisplay.bind(this), + 'move-cursor': this.moveCursor.bind(this), + 'change-view': this.changeView.bind(this), + 'resize-display': this.resizeDisplay.bind(this), + 'bulk': this.bulk.bind(this), + 'exit': this.exitEditor.bind(this), + 'user-input': this.userInput.bind(this), + 'switch-file': this.switchFile.bind(this), + 'get-clipboard-text': this.getClipboardText.bind(this), + 'set-clipboard-text': this.setClipboardText.bind(this), + 'js-eval': this.jsEval.bind(this), + }); + + this.login(); + + this.boundedHandleResize = this.handleResize.bind(this); + } + + init() { + window.addEventListener('resize', this.boundedHandleResize); + document.getElementsByTagName('html')[0].style['background-color'] = '#333'; + } + + finalize() { + window.removeEventListener('resize', this.boundedHandleResize); + this.input.finalize(); + } + + closeConnection() { + this.jsonrpc.close(); + } + + emitInput(event) { + const key = keyevent.convertKeyEvent(event); + if (!key) return; + + if (key.key === ']' && key.ctrl && !key.meta && !key.super && !key.shift) { + this.jsonrpc.notify('input', { kind: 'abort' }); + return; + } + + this.jsonrpc.notify('input', { kind: 'key', value: key }); + } + + emitInputString(string) { + if (string) { + this.jsonrpc.notify('input', { kind: 'input-string', value: string }); + } else { + console.error('unexpected argument', string); + } + } + + handleResize(event) { + const canResize = true; + if (canResize) { + this.jsonrpc.notify('redraw', { size: this.getDisplaySize() }); + } else { + this.jsonrpc.notify('redraw'); + } + } + + enableInput() { + this.inputEnabled = true; + } + + disableInput() { + this.inputEnabled = false; + } + + sendNotification(method, args) { + this.jsonrpc.notify(method, args); + } + + request(method, args, callback) { + this.jsonrpc.request(method, args, callback); + } + + getDisplaySize() { + const [_x, _y, displayWidth, displayHeight] = this.getDisplayRectangle(); + const width = Math.round(displayWidth / this.option.fontWidth); + const height = Math.round(displayHeight / this.option.fontHeight); + return { width, height }; + } + + callMessage(method, argument) { + this.messageTable.get(method)(argument); + } + + findViewById(id) { + return this.viewMap.get(id); + } + + login() { + this.jsonrpc.request('login', { + size: this.getDisplaySize(), + foreground: this.option.foreground, + background: this.option.background, + }, (response) => { + this.updateForeground(response.foreground); + this.updateBackground(response.background); + if (response.views) { + for (const view of response.views) { + this.makeView(view); + } + } + + this.jsonrpc.notify('redraw', { size: this.getDisplaySize() }); + + this.jsonrpc.request('user-file-map', {}, response => { + this.onSwitchFile(response); + }); + }); + } + + startup() { + if (this.onRestart) { + this.onRestart(); + } + } + + updateForeground(color) { + this.option.foreground = color; + this.input.updateForeground(color); + } + + updateBackground(color) { + this.option.background = color; + this.input.updateBackground(color); + const element = getLemEditorElement(); + element.style.backgroundColor = color; + } + + makeView({ id, x, y, width, height, use_modeline, kind, type, content, border }) { + const view = new View({ + option: this.option, + id: id, + x: x, + y: y, + width: width, + height: height, + useModeline: use_modeline, + kind: kind, + type: type, + content: content, + border: border, + editor: this + }); + this.viewMap.set(id, view); + } + + deleteView({ viewInfo: { id } }) { + const view = this.findViewById(id); + view.delete(); + this.viewMap.delete(id); + } + + resize({ viewInfo: { id }, width, height }) { + const view = this.findViewById(id); + view.resize(width, height); + } + + move({ viewInfo: { id }, x, y }) { + const view = this.findViewById(id); + view.move(x, y); + } + + redrawViewAfter({ viewInfo: { id }, html }) { + const view = this.findViewById(id); + view.touch(); + } + + clear({ viewInfo: { id } }) { + const view = this.findViewById(id); + view.clear(); + } + + clearEol({ viewInfo: { id }, x, y }) { + const view = this.findViewById(id); + view.clearEol(x, y); + } + + clearEob({ viewInfo: { id }, x, y }) { + const view = this.findViewById(id); + view.clearEob(x, y); + } + + put({ viewInfo: { id }, x, y, text, textWidth, attribute, cursorInfo }) { + const view = this.findViewById(id); + view.print(x, y, text, textWidth, attribute); + if (cursorInfo) { + const { name, color } = cursorInfo; + let cursor = this.cursors.get(name); + if (!cursor) { + cursor = new Cursor(this, name, color); + this.cursors.set(name, cursor); + } + + cursor.move( + (view.x + x) * this.option.fontWidth, + (view.y + y - 1) * this.option.fontHeight, + ); + } + } + + modelinePut({ viewInfo: { id }, x, y, text, textWidth, attribute }) { + const view = this.findViewById(id); + view.printToModeline(x, y, text, textWidth, attribute); + } + + updateDisplay() { + } + + moveCursor({ viewInfo: { id }, x, y }) { + const view = this.findViewById(id); + const left = view.x * this.option.fontWidth + x * this.option.fontWidth; + const top = view.y * this.option.fontHeight + y * this.option.fontHeight; + this.input.move(left, top); + } + + changeView({ viewInfo: { id }, type, content }) { + const view = this.findViewById(id); + switch (type) { + case 'html': + view.changeToHTMLContent(content); + break; + case 'editor': + view.changeToEditorContent(); + break; + } + } + + resizeDisplay({ width, height }) { + // TODO: offset + const element = getLemEditorElement(); + element.style.width = Math.floor(width * this.option.fontWidth) + 'px'; + element.style.height = Math.floor(height * this.option.fontHeight) + 'px'; + } + + bulk(messages) { + if (this.onLoaded) { + this.onLoaded(); + this.onLoaded = null; + } + for (const { method, argument } of messages) { + this.callMessage(method, argument); + } + } + + exitEditor() { + if (this.onExit) { + this.onExit(); + } + } + + userInput({ value }) { + if (this.onUserInput) { + this.onUserInput(value); + } + } + + switchFile(userFileMap) { + if (this.onSwitchFile) { + this.onSwitchFile(userFileMap); + } + } + + getClipboardText() { + navigator.clipboard.readText().then(text => { + this.jsonrpc.notify('got-clipboard-text', { text }); + }); + } + + setClipboardText({ text }) { + if (navigator.clipboard) { + navigator.clipboard.writeText(text); + } + } + + jsEval({ viewInfo: { id }, code }) { + const view = this.findViewById(id); + view.evalIn(code); + } +} diff --git a/frontends/server/frontend/index.html b/frontends/server/frontend/index.html new file mode 100644 index 000000000..0e177994b --- /dev/null +++ b/frontends/server/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + Lem + + + + + + +
+ + diff --git a/frontends/server/frontend/jsonrpc.js b/frontends/server/frontend/jsonrpc.js new file mode 100644 index 000000000..ff3a21205 --- /dev/null +++ b/frontends/server/frontend/jsonrpc.js @@ -0,0 +1,124 @@ +"use strict"; + +import { + JSONRPCServerAndClient, + JSONRPCServer, + JSONRPCClient, +} from "json-rpc-2.0"; + +export class JSONRPC { + constructor(url, { onConnected, onClosed }) { + this.url = url; + this.onConnected = onConnected; + this.onClosed = onClosed; + + this.messageQueue = []; + this.serverAndClient = null; + this.connect(); + this.connectionEstablished = false; + + this.timerId = null; + this.closed = false; + } + + close() { + if (this.timerId) { + clearTimeout(this.timerId); + } + this.webSocket.close(); + this.closed = true; + } + + on(method, handler) { + this.serverAndClient.addMethod(method, handler); + } + + async requestInternal(method, arg, callback) { + const result = await this.serverAndClient.request(method, arg); + if (callback) { + callback(result); + } + } + + requestMessageQueue() { + this.messageQueue.forEach((value) => { + const [method, arg, callback] = value; + this.requestInternal(method, arg, callback); + }); + this.messageQueue = []; + } + + request(method, arg, callback) { + if (this.webSocket.readyState === WebSocket.OPEN) { + this.requestInternal(method, arg, callback); + } else { + this.messageQueue.push([method, arg, callback]); + } + } + + notify(method, arg) { + //console.log('notify', this.webSocket.readyState); + switch (this.webSocket.readyState) { + case WebSocket.OPEN: + this.serverAndClient.notify(method, arg); + break; + case WebSocket.CLOSED: + break; + } + } + + connect(webSocket) { + if (this.closed) return; + + console.log('connect', this.url); + this.webSocket = new WebSocket(this.url); + + if (!this.serverAndClient) { + this.serverAndClient = new JSONRPCServerAndClient( + new JSONRPCServer(), + new JSONRPCClient((request) => { + try { + //console.log(request); + this.webSocket.send(JSON.stringify(request)); + return Promise.resolve(); + } catch (error) { + return Promise.reject(error); + } + }) + ); + } + + this.webSocket.onmessage = (event) => { + this.serverAndClient.receiveAndSend(JSON.parse(event.data.toString())); + }; + + this.webSocket.onopen = () => { + console.log("WebSocket connection established"); + this.connectionEstablished = true; + if (this.onConnected) { + this.onConnected(); + } + this.requestMessageQueue(); + }; + + this.webSocket.onclose = (event) => { + console.error("WebScoket closed", event); + this.serverAndClient.rejectAllPendingRequests( + `Connection is closed (${event.reason}).` + ); + + if (this.connectionEstablished) { + this.onClosed(); + } + + this.timerId = setTimeout(() => { + this.connect(); + }, 3000); + }; + + this.webSocket.onerror = (error) => { + console.error("WebSocket error:", error); + this.webSocket.close(); + }; + } +} diff --git a/frontends/server/frontend/keyevent.js b/frontends/server/frontend/keyevent.js new file mode 100644 index 000000000..d4d4a61b1 --- /dev/null +++ b/frontends/server/frontend/keyevent.js @@ -0,0 +1,134 @@ +"use strict"; + +const modifierKeys = ["Shift", "Control", "Alt", "Meta"]; + +const convertKeyTable = { + Enter: "Return", + ArrowRight: "Right", + ArrowLeft: "Left", + ArrowUp: "Up", + ArrowDown: "Down", + + '¡': '1', + '™': '2', + '£': '3', + '¢': '4', + '∞': '5', + '§': '6', + '¶': '7', + '•': '8', + 'ª': '9', + 'º': '0', + '–': '-', + '≠': '=', + '“': '[', + '‘': ']', + '«': '\\', + '…': ';', + 'æ': '\'', + '≤': ',', + '≥': '.', + '÷': '/', + '⁄': '!', + '€': '@', + '‹': '#', + '›': '$', + 'fi': '%', + 'fl': '^', + '‡': '&', + '°': '*', + '·': '(', + '‚': ')', + '—': '_', + '±': '+', + '”': '{', + '’': '}', + '»': '|', + 'Ú': ':', + 'Æ': '"', + '¯': '<', + '˘': '>', + '¿': '?', + + 'œ': 'q', + '∑': 'w', + '´': 'e', + '®': 'r', + '†': 't', + '¥': 'y', + '¨': 'u', + 'ˆ': 'i', + 'ø': 'o', + 'π': 'p', + 'å': 'a', + 'ß': 's', + '∂': 'd', + 'ƒ': 'f', + '©': 'g', + '˙': 'h', + '∆': 'j', + '˚': 'k', + '¬': 'l', + 'Ω': 'z', + '≈': 'x', + 'ç': 'c', + '√': 'v', + '∫': 'b', + '˜': 'n', + 'µ': 'm', + + 'Œ': 'Q', + '„': 'W', + '´': 'E', + '‰': 'R', + 'ˇ': 'T', + 'Á': 'Y', + '¨': 'U', + 'ˆ': 'I', + 'Ø': 'O', + '∏': 'P', + 'Å': 'A', + 'Í': 'S', + 'Î': 'D', + 'Ï': 'F', + '˝': 'G', + 'Ó': 'H', + 'Ô': 'J', + '': 'K', + 'Ò': 'L', + '¸': 'Z', + '˛': 'X', + 'Ç': 'C', + '◊': 'V', + 'ı': 'B', + '˜': 'N', + 'Â': 'M', +}; + +function getKey(e) { + if (e.altKey) { + return ( + convertKeyTable[e.key] || + (e.code.startsWith('Key') ? e.code[3].toLowerCase() : null) || + e.key + ); + } + + return convertKeyTable[e.key] || e.key; +} + +export function convertKeyEvent (e) { + if (modifierKeys.indexOf(e.key) !== -1) { + return null; + } + + const key = getKey(e); + + return { + key: key, + ctrl: e.ctrlKey, + meta: e.altKey, + super: e.metaKey, + shift: e.shiftKey, + }; +}; diff --git a/frontends/server/frontend/main.js b/frontends/server/frontend/main.js new file mode 100644 index 000000000..0eca49a82 --- /dev/null +++ b/frontends/server/frontend/main.js @@ -0,0 +1,23 @@ +import { Editor } from './editor.js'; + +const canvas = document.querySelector('#editor'); + +function main() { + document.fonts.ready.then(() => { + const editor = new Editor({ + canvas: canvas, + fontName: 'Monospace', + fontSize: 19, + onLoaded: null, + url: `ws://${window.location.hostname}:50000`, + onExit: null, + onClosed: null, + onRestart: null, + onUserInput: null, + }); + + editor.init(); + }); +} + +main(); diff --git a/frontends/server/frontend/package-lock.json b/frontends/server/frontend/package-lock.json new file mode 100644 index 000000000..72d9be568 --- /dev/null +++ b/frontends/server/frontend/package-lock.json @@ -0,0 +1,814 @@ +{ + "name": "editor", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "editor", + "version": "0.0.0", + "dependencies": { + "json-rpc-2.0": "^1.7.0", + "meaw": "^8.0.1" + }, + "devDependencies": { + "vite": "^5.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/json-rpc-2.0": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.7.0.tgz", + "integrity": "sha512-asnLgC1qD5ytP+fvBP8uL0rvj+l8P6iYICbzZ8dVxCpESffVjzA7KkYkbKCIbavs7cllwH1ZUaNtJwphdeRqpg==" + }, + "node_modules/meaw": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/meaw/-/meaw-8.0.1.tgz", + "integrity": "sha512-yBDo3rgkH2IIG09/xgMJJE7J6A5zYzwdcDW7NuGcz3DEnQqJU1BuxGd/x/nQMqKrIZhFU3sJTfblra30EI/rkg==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.2.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", + "integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==", + "dev": true, + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/frontends/server/frontend/package.json b/frontends/server/frontend/package.json new file mode 100644 index 000000000..0ceaa6584 --- /dev/null +++ b/frontends/server/frontend/package.json @@ -0,0 +1,18 @@ +{ + "name": "editor", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^5.2.0" + }, + "dependencies": { + "json-rpc-2.0": "^1.7.0", + "meaw": "^8.0.1" + } +} diff --git a/frontends/server/jsonrpc-stdio-patch.lisp b/frontends/server/jsonrpc-stdio-patch.lisp new file mode 100644 index 000000000..a53da1b60 --- /dev/null +++ b/frontends/server/jsonrpc-stdio-patch.lisp @@ -0,0 +1,28 @@ +(in-package :jsonrpc/transport/stdio) + +(defmethod send-message-using-transport ((transport stdio-transport) connection message) + (let ((json (babel:string-to-octets + (with-output-to-string (s) + (yason:encode message s)))) + (stream (connection-socket connection))) + (format stream "Content-Length: ~A~C~C~:*~:*~C~C" + (length json) + #\Return + #\Newline) + (write-sequence json stream) + (force-output stream))) + +(defmethod receive-message-using-transport ((transport stdio-transport) connection) + (let* ((stream (connection-socket connection)) + (headers (read-headers stream)) + (length (ignore-errors (parse-integer (gethash "content-length" headers))))) + (when length + (let ((body + (with-output-to-string (out) + (loop + :for c := (read-char stream) + :do (write-char c out) + (decf length (babel:string-size-in-octets (string c))) + (when (<= length 0) + (return)))))) + (parse-message body))))) diff --git a/frontends/server/lem-server.asd b/frontends/server/lem-server.asd new file mode 100644 index 000000000..b01dcd450 --- /dev/null +++ b/frontends/server/lem-server.asd @@ -0,0 +1,15 @@ +(defsystem "lem-server" + :depends-on ("lem" + "lem/extensions" + "jsonrpc" + "jsonrpc/transport/stdio" + "jsonrpc/transport/websocket" + "jsonrpc/transport/local-domain-socket" + "command-line-arguments") + :serial t + :components ((:file "jsonrpc-stdio-patch") + (:file "config") + (:file "utils") + (:file "view") + (:file "mouse") + (:file "main"))) diff --git a/frontends/server/main.lisp b/frontends/server/main.lisp new file mode 100644 index 000000000..9908caa52 --- /dev/null +++ b/frontends/server/main.lisp @@ -0,0 +1,693 @@ +(defpackage :lem-server + (:use :cl + :lem-server/utils + :lem-server/view) + (:local-nicknames (:display :lem-core/display) + (:queue :lem/common/queue) + (:mouse :lem-server/mouse)) + (:export :run-tcp-server + :run-stdio-server + :run-websocket-server + :main)) +(in-package :lem-server) + +(defvar *server-runner*) + +(defclass server-runner () + ()) + +;;; +(defclass websocket-server-runner (server-runner) + ((port :initarg :port + :reader websocket-server-runner-port) + (host :initarg :host + :reader websocket-server-runner-host))) + +(defmethod server-listen ((runner websocket-server-runner) server) + (jsonrpc:server-listen server + :mode :websocket + :port (websocket-server-runner-port runner) + :host (websocket-server-runner-host runner) + :clack-handler 'clack-handler)) + +(defun clack-handler (env) + (unless (wsd:websocket-p env) + (let ((path (getf env :path-info))) + (cond ((string= "/" path) + `(200 (:content-type "text/html") + ,(asdf:system-relative-pathname :lem-server + #p"frontend/dist/index.html"))) + ((alexandria:starts-with-subseq "/assets/" path) + `(200 (:content-type "application/javascript") + ,(asdf:system-relative-pathname :lem-server + (format nil + "frontend/dist/~A" + (string-left-trim "/" path))))) + (t + '(200 () ("ok"))))))) + +;;; +(defclass stdio-server-runner (server-runner) + ()) + +(defmethod server-listen ((runner stdio-server-runner) server) + (jsonrpc:server-listen server + :mode :stdio)) + +;;; +(defclass local-domain-socket-server-runner (server-runner) + ((address :initarg :address :reader local-domain-socket-server-runner-address))) + +(defmethod server-listen ((runner local-domain-socket-server-runner) server) + (jsonrpc:server-listen server + :mode :local-domain-socket + :address (local-domain-socket-server-runner-address runner))) + +(defclass server (jsonrpc:server) ()) + +(defclass jsonrpc (lem:implementation) + ((server :initform (make-instance 'server) + :reader jsonrpc-server) + (display-width :initform 80 + :accessor jsonrpc-display-width) + (display-height :initform 24 + :accessor jsonrpc-display-height) + (background-color :accessor jsonrpc-background-color) + (foreground-color :accessor jsonrpc-foreground-color) + (message-queue :initform (queue:make-queue) + :reader jsonrpc-message-queue) + (editor-thread :initform nil + :accessor jsonrpc-editor-thread)) + (:default-initargs + :name :jsonrpc + :redraw-after-modifying-floating-window t + :window-left-margin 1)) + +(defun get-all-views () + (if (null (lem:current-frame)) + (vector) + (coerce + (loop :for frame :in (lem:all-frames) + :append (loop :for window :in (append (lem:frame-header-windows frame) + (lem:window-list frame) + (lem:frame-floating-windows frame)) + :collect (lem:window-view window))) + 'vector))) + +(defmethod resize-display ((jsonrpc jsonrpc) width height) + (setf (jsonrpc-display-width jsonrpc) width + (jsonrpc-display-height jsonrpc) height)) + +(defmethod notify ((jsonrpc jsonrpc) method argument) + (jsonrpc:broadcast (jsonrpc-server jsonrpc) method argument)) + +(defmethod notify* ((jsonrpc jsonrpc) method argument) + (queue:enqueue (jsonrpc-message-queue jsonrpc) + (hash "method" method "argument" argument))) + +(defmethod notify-all ((jsonrpc jsonrpc)) + (let ((argument (coerce (loop :until (queue:empty-p (jsonrpc-message-queue jsonrpc)) + :collect (queue:dequeue (jsonrpc-message-queue jsonrpc))) + 'vector))) + (notify jsonrpc "bulk" argument))) + +(defun handle-login (jsonrpc logged-in-callback params) + (pdebug "ready: ~A ~A" jsonrpc/connection:*connection* (pretty-json params)) + (with-error-handler () + (let* ((size (gethash "size" params)) + (foreground (gethash "foreground" params)) + (background (gethash "background" params))) + + (when size + (let ((width (gethash "width" size)) + (height (gethash "height" size))) + (resize-display jsonrpc width height))) + (when background + (alexandria:when-let (color (lem:parse-color background)) + (setf (jsonrpc-background-color jsonrpc) color))) + (when foreground + (alexandria:when-let (color (lem:parse-color foreground)) + (setf (jsonrpc-foreground-color jsonrpc) color))) + (funcall logged-in-callback) + + (let ((response (hash "views" (with-error-handler () (get-all-views)) + "foreground" (lem-core::foreground-color) + "background" (lem-core::background-color) + "size" (hash "width" (lem:display-width) + "height" (lem:display-height))))) + (pdebug "login response: ~A" (pretty-json response)) + response)))) + +(defun login (jsonrpc logged-in-callback) + (lambda (params) + (handle-login jsonrpc logged-in-callback params))) + +(defun redraw (args) + (pdebug "redraw: ~A" (pretty-json args)) + (with-error-handler () + (let ((size (and args (gethash "size" args)))) + (when size + (let ((width (gethash "width" size)) + (height (gethash "height" size))) + (resize-display (lem:implementation) width height) + (notify (lem:implementation) "resize-display" size))) + (lem:send-event (lambda () + (lem-core::adjust-all-window-size) + (lem:redraw-display :force t)))))) + +(defmethod lem-if:invoke ((jsonrpc jsonrpc) function) + (let ((ready nil)) + (setf (jsonrpc-editor-thread jsonrpc) + (funcall function + (lambda () + (loop :until ready) + (notify jsonrpc "startup" nil)))) + (jsonrpc:expose (jsonrpc-server jsonrpc) + "login" + (login jsonrpc + (lambda () + (setf ready t)))) + (jsonrpc:expose (jsonrpc-server jsonrpc) + "input" + (lambda (args) + (input-callback jsonrpc args))) + (jsonrpc:expose (jsonrpc-server jsonrpc) + "redraw" + 'redraw) + (jsonrpc:expose (jsonrpc-server jsonrpc) + "got-clipboard-text" + 'got-clipboard-text) + + (lem:add-hook lem:*exit-editor-hook* + (lambda () + (notify jsonrpc "exit" nil) + (uiop:quit 0))) + + (server-listen *server-runner* (jsonrpc-server jsonrpc)))) + +(defmethod lem-if:get-background-color ((jsonrpc jsonrpc)) + (jsonrpc-background-color jsonrpc)) + +(defmethod lem-if:get-foreground-color ((jsonrpc jsonrpc)) + (jsonrpc-foreground-color jsonrpc)) + +(defmethod lem-if:update-foreground ((jsonrpc jsonrpc) color-name) + (with-error-handler () + (notify jsonrpc "update-foreground" color-name))) + +(defmethod lem-if:update-background ((jsonrpc jsonrpc) color-name) + (with-error-handler () + (notify jsonrpc "update-background" color-name))) + +(defmethod lem-if:update-cursor-shape ((jsonrpc jsonrpc) cursor-type) + ;; TODO + ) + +(defmethod lem-if:display-width ((jsonrpc jsonrpc)) + (with-error-handler () + (jsonrpc-display-width jsonrpc))) + +(defmethod lem-if:display-height ((jsonrpc jsonrpc)) + (with-error-handler () + (jsonrpc-display-height jsonrpc))) + +(defmethod lem-if:display-title ((jsonrpc jsonrpc)) + ;; TODO + ) + +(defmethod lem-if:set-display-title ((jsonrpc jsonrpc) title) + ;; TODO + ) + +(defmethod lem-if:display-fullscreen-p ((jsonrpc jsonrpc)) + ;; TODO + ) + +(defmethod lem-if:set-display-fullscreen-p ((jsonrpc jsonrpc) fullscreen-p) + ;; TODO + ) + +(defmethod lem-if:make-view ((jsonrpc jsonrpc) window x y width height use-modeline) + (let ((view (make-view :window window + :x x + :y y + :width width + :height height + :use-modeline use-modeline + :kind (if (lem:floating-window-p window) + "floating" + "tile") + :border (and (lem:floating-window-p window) + (lem:floating-window-border window))))) + (notify* jsonrpc "make-view" view) + view)) + +(defmethod lem-if:view-width ((jsonrpc jsonrpc) view) + (view-width view)) + +(defmethod lem-if:view-height ((jsonrpc jsonrpc) view) + (view-height view)) + +(defmethod lem-if:delete-view ((jsonrpc jsonrpc) view) + (with-error-handler () + (notify* jsonrpc "delete-view" (hash "viewInfo" view)))) + +(defmethod lem-if:clear ((jsonrpc jsonrpc) view) + (with-error-handler () + (notify* jsonrpc "clear" (hash "viewInfo" view)))) + +(defmethod lem-if:set-view-size ((jsonrpc jsonrpc) view width height) + (with-error-handler () + (resize-view view width height) + (notify* jsonrpc + "resize-view" + (hash "viewInfo" view + "width" width + "height" height)))) + +(defmethod lem-if:set-view-pos ((jsonrpc jsonrpc) view x y) + (with-error-handler () + (move-view view x y) + (notify* jsonrpc + "move-view" + (hash "viewInfo" view + "x" x + "y" y)))) + +(defmethod lem-if:redraw-view-before ((jsonrpc jsonrpc) view) + ) + +(defmethod lem-if:redraw-view-after ((jsonrpc jsonrpc) view) + (notify* jsonrpc + "redraw-view-after" + (hash "viewInfo" view))) + +(defmethod lem:redraw-buffer ((jsonrpc jsonrpc) (buffer lem:html-buffer) window force) + ) + +(defmethod lem-if:will-update-display ((jsonrpc jsonrpc)) + ) + +(defmethod lem-if:update-display ((jsonrpc jsonrpc)) + (with-error-handler () + (let ((view (lem:window-view (lem:current-window))) + (x (lem:last-print-cursor-x (lem:current-window))) + (y (lem:last-print-cursor-y (lem:current-window)))) + (notify* jsonrpc + "move-cursor" + (hash "viewInfo" view "x" x "y" y))) + (notify* jsonrpc "update-display" nil) + (notify-all jsonrpc))) + +(defvar *clipboard-wait-queue* (lem/common/queue:make-concurrent-queue)) + +(defmethod lem-if:clipboard-paste ((jsonrpc jsonrpc)) + (notify jsonrpc "get-clipboard-text" (hash)) + (lem/common/queue:dequeue *clipboard-wait-queue* :timeout 0.1)) + +(defun got-clipboard-text (params) + (let ((text (gethash "text" params))) + (lem/common/queue:enqueue *clipboard-wait-queue* text))) + +(defmethod lem-if:clipboard-copy ((jsonrpc jsonrpc) text) + (notify jsonrpc "set-clipboard-text" (hash "text" text))) + +(defmethod lem-if:increase-font-size ((jsonrpc jsonrpc)) + ;; TODO + ) + +(defmethod lem-if:decrease-font-size ((jsonrpc jsonrpc)) + ;; TODO + ) + +(defmethod lem-if:resize-display-before ((jsonrpc jsonrpc)) + ) + +(defmethod lem-if:get-font-list ((jsonrpc jsonrpc)) + ) + +(defmethod lem-if:get-mouse-position ((jsonrpc jsonrpc)) + (mouse:get-position)) + +(defmethod lem-if:get-char-width ((jsonrpc jsonrpc)) + ;; TODO + 1) +(defmethod lem-if:get-char-height ((jsonrpc jsonrpc)) + ;; TODO + 1) + +(defmethod lem-if:js-eval ((jsonrpc jsonrpc) view code) + (notify (lem:implementation) + "js-eval" + (hash "viewInfo" view + "code" code))) + +(lem:add-hook lem:*switch-to-buffer-hook* 'on-switch-to-buffer) + +(defun on-switch-to-buffer (buffer) + (cond ((and (typep buffer 'lem:html-buffer) + (lem:html-buffer-updated-p buffer)) + (lem:invalidate-html-buffer-updated buffer) + (notify* (lem:implementation) + "change-view" + (hash "viewInfo" (lem:window-view (lem:current-window)) + "type" "html" + "content" (lem:html-buffer-html buffer)))) + ((and (typep (lem:current-buffer) 'lem:html-buffer) + (not (typep buffer 'lem:html-buffer))) + (notify* (lem:implementation) + "change-view" + (hash "viewInfo" (lem:window-view (lem:current-window)) + "type" "editor"))) + ((and (not (typep (lem:current-buffer) 'lem:html-buffer)) + (typep buffer 'lem:html-buffer)) + (notify* (lem:implementation) + "change-view" + (hash "viewInfo" (lem:window-view (lem:current-window)) + "type" "html" + "content" (lem:html-buffer-html buffer)))))) +;;;; +(defun bool (x) (if x 'yason:true 'yason:false)) + +(defun ensure-rgb (color) + (if (typep color 'lem:color) + (lem:color-to-hex-string color) + color)) + +(defmethod yason:encode ((attribute lem:attribute) &optional (stream *standard-output*)) + (with-error-handler () + (yason:with-output (stream) + (yason:with-object () + (yason:encode-object-element "foreground" (ensure-rgb (lem:attribute-foreground attribute))) + (yason:encode-object-element "background" (ensure-rgb (lem:attribute-background attribute))) + (yason:encode-object-element "reverse" (bool (lem:attribute-reverse attribute))) + (yason:encode-object-element "bold" (bool (lem:attribute-bold attribute))) + (yason:encode-object-element "underline" (lem:attribute-underline attribute)))))) + + + +;;; drawing +(defgeneric object-width (drawing-object)) + +(defmethod object-width ((drawing-object display:void-object)) + 0) + +(defmethod object-width ((drawing-object display:text-object)) + (lem-core:string-width (display:text-object-string drawing-object))) + +(defmethod object-width ((drawing-object display:eol-cursor-object)) + 0) + +(defmethod object-width ((drawing-object display:extend-to-eol-object)) + 0) + +(defmethod object-width ((drawing-object display:line-end-object)) + 0) + +(defmethod object-width ((drawing-object display:image-object)) + 0) + +(defgeneric draw-object (jsonrpc object x y view)) + +(defmethod draw-object (jsonrpc (object display:void-object) x y view) + (values)) + +(defvar *put-target* :edit-area) + +(defun put (jsonrpc view x y string attribute) + (with-error-handler () + (notify* jsonrpc + (ecase *put-target* + (:edit-area "put") + (:modeline "modeline-put")) + (hash "viewInfo" view + "x" x + "y" y + "text" string + "textWidth" (lem:string-width string) + "attribute" (lem:ensure-attribute attribute nil))))) + +(defmethod draw-object (jsonrpc (object display:text-object) x y view) + (let* ((string (display:text-object-string object)) + (attribute (display:text-object-attribute object))) + (when (and attribute (lem-core:cursor-attribute-p attribute)) + (lem-core::set-last-print-cursor (view-window view) x y)) + (put jsonrpc view x y string attribute))) + +(defmethod draw-object (jsonrpc (object display:eol-cursor-object) x y view) + (lem-core::set-last-print-cursor (view-window view) x y) + (put jsonrpc view x y " " + (lem:make-attribute + :background + (lem:color-to-hex-string (display:eol-cursor-object-color object))))) + +(defmethod draw-object (jsonrpc (object display:extend-to-eol-object) x y view) + (let ((width (lem-if:view-width (lem-core:implementation) view))) + (when (< x width) + (put jsonrpc view x y + (make-string (- width x) :initial-element #\space) + (lem:make-attribute + :background + (lem:color-to-hex-string (display:extend-to-eol-object-color object))))))) + +(defmethod draw-object (jsonrpc (object display:line-end-object) x y view) + (let ((string (display:text-object-string object)) + (attribute (display:text-object-attribute object))) + (put jsonrpc + view + (+ x (display:line-end-object-offset object)) + y + string + attribute))) + +(defmethod draw-object (jsonrpc (object display:image-object) x y view) + (values)) + +(defun render-line (jsonrpc view x y objects) + (loop :for object :in objects + :do (draw-object jsonrpc object x y view) + (incf x (object-width object)))) + +(defun render-line-from-behind (jsonrpc view y objects) + (loop :with current-x := (view-width view) + :for object :in objects + :do (decf current-x (object-width object)) + (draw-object jsonrpc object current-x y view))) + +(defmethod lem-if:render-line ((jsonrpc jsonrpc) view x y objects height) + (with-error-handler () + (notify* jsonrpc + "clear-eol" + (hash "viewInfo" view + "x" x + "y" y)) + (render-line jsonrpc view x y objects))) + +(defmethod lem-if:render-line-on-modeline ((jsonrpc jsonrpc) view left-objects right-objects + default-attribute height) + (let ((*put-target* :modeline)) + (with-error-handler () + (notify* jsonrpc + "modeline-put" + (hash "viewInfo" view + "x" 0 + "y" 0 + "text" (make-string (view-width view) :initial-element #\space) + "textWidth" (view-width view) + "attribute" default-attribute)) + (render-line jsonrpc view 0 0 left-objects) + (render-line-from-behind jsonrpc view 0 right-objects)))) + +(defmethod lem-if:object-width ((jsonrpc jsonrpc) drawing-object) + (object-width drawing-object)) + +(defmethod lem-if:object-height ((jsonrpc jsonrpc) drawing-object) + 1) + +(defmethod lem-if:clear-to-end-of-window ((jsonrpc jsonrpc) view y) + (notify* jsonrpc + "clear-eob" + (hash "viewInfo" view + "x" 0 + "y" y))) + + +;;; +(defconstant +abort+ 0) +(defconstant +keyevent+ 1) +(defconstant +resize+ 2) +(defconstant +input-string+ 3) + +(defun convert-keyevent (e) + (let ((key (gethash "key" e)) + (ctrl (gethash "ctrl" e)) + (meta (gethash "meta" e)) + (super (gethash "super" e)) + (shift (gethash "shift" e))) + (cond ((string= key " ") (setf key "Space"))) + (lem:make-key :ctrl ctrl + :meta meta + :super super + :shift (if (lem:insertion-key-sym-p key) + nil + shift) + :sym (if (and (lem:insertion-key-sym-p key) + shift + meta) + (string-upcase key) + key)))) + +(defun convert-button (button) + (case button + (0 :button-1) + (2 :button-3) + (1 :button-2))) + +(defun input-callback (jsonrpc args) + (handler-case + (let ((kind (gethash "kind" args)) + (value (gethash "value" args))) + (alexandria:switch (kind :test #'equal) + ("abort" + (lem:send-abort-event (jsonrpc-editor-thread jsonrpc) nil)) + ("key" + (when value + (notify jsonrpc + "user-input" + (hash "value" value)) + (let ((key (convert-keyevent value))) + (lem:send-event key)))) + ("clipboard-paste" + (lem:send-event + (lambda () + (let ((text (lem:get-clipboard-data)) + (mode (lem:ensure-mode-object + (lem:current-major-mode-at-point + (lem:current-point))))) + (lem:paste-using-mode mode text))))) + ("mousedown" + (let ((x (gethash "x" value)) + (y (gethash "y" value)) + (pixel-x (gethash "pixelX" value)) + (pixel-y (gethash "pixelY" value)) + (button (convert-button (gethash "button" value))) + (clicks (gethash "clicks" value))) + (lem:send-event + (lambda () + (lem:receive-mouse-button-down x + y + pixel-x + pixel-y + button + clicks))))) + ("mouseup" + (let ((x (gethash "x" value)) + (y (gethash "y" value)) + (pixel-x (gethash "pixelX" value)) + (pixel-y (gethash "pixelY" value)) + (button (convert-button (gethash "button" value)))) + (when button + (lem:send-event + (lambda () + (lem:receive-mouse-button-up x + y + pixel-x + pixel-y + button)))))) + ("mousemove" + (let ((x (gethash "x" value)) + (y (gethash "y" value)) + (pixel-x (gethash "pixelX" value)) + (pixel-y (gethash "pixelY" value)) + (button (convert-button (gethash "button" value)))) + (lem:send-event + (lambda () + (mouse:update-position x y) + (lem:receive-mouse-motion x + y + pixel-x + pixel-y + button))))) + ("wheel" + (let ((x (gethash "x" value)) + (y (gethash "y" value)) + (pixel-x (gethash "pixelX" value)) + (pixel-y (gethash "pixelY" value)) + (wheel-x (gethash "wheelX" value)) + (wheel-y (gethash "wheelY" value))) + (lem:send-event + (lambda () + (lem:receive-mouse-wheel x y pixel-x pixel-y wheel-x wheel-y) + (when (= 0 (lem:event-queue-length)) + (lem:redraw-display)))))) + ("resize" + (resize-display jsonrpc + (gethash "width" value) + (gethash "height" value)) + (lem:send-event :resize)) + ("input-string" + (notify jsonrpc + "user-input" + (hash "value" value)) + (loop :for c :across value + :for key := (convert-keyevent + (alexandria:plist-hash-table (list "key" (string c)) + :test 'equal)) + :do (lem:send-event key))) + (otherwise (error "unexpected input kind: ~D" kind)))) + (error (e) + (pdebug "input-callback: ~A ~A" e + (with-output-to-string (stream) + (let ((stream (yason:make-json-output-stream stream))) + (yason:encode args stream))))))) + +;;; +(defparameter +command-line-spec+ + '(("mode" :type string :optional t :documentation "\"websocket\", \"stdio\", \"local-domain-socket\"") + ("port" :type integer :optional nil :documentation "port of \"websocket\"") + ("host" :type string :optional t) + ("address" :type string :optional t :documentation "address of \"local-domain-socket\""))) + +(defun run-websocket-server (&key (port 50000) (hostname "127.0.0.1")) + (let ((*server-runner* + (make-instance 'websocket-server-runner + :port port + :host hostname))) + (lem:lem))) + +(defun run-stdio-server () + (let ((*server-runner* (make-instance 'stdio-server-runner))) + (lem:lem))) + +(defun run-local-domain-socket-server (&key address) + (let ((*server-runner* (make-instance 'local-domain-socket-server-runner + :address address))) + (lem:lem))) + +(defun check-port-specified (port) + (unless port + (command-line-arguments:show-option-help +command-line-spec+) + (uiop:quit 1))) + +(defun main (&optional (args (uiop:command-line-arguments))) + (command-line-arguments:handle-command-line + +command-line-spec+ + (lambda (&key (mode "websocket") + port + (host "127.0.0.1") + address) + (vom:config t :info) + (cond ((string= mode "websocket") + (check-port-specified port) + (run-websocket-server :port port + :hostname host)) + ((string= mode "stdio") + (run-stdio-server)) + ((string= mode "local-domain-socket") + (run-local-domain-socket-server :address address)) + (t + (command-line-arguments:show-option-help +command-line-spec+) + (uiop:quit 1)))) + :name "lem-server" + :positional-arity 0 + :command-line args)) diff --git a/frontends/server/mouse.lisp b/frontends/server/mouse.lisp new file mode 100644 index 000000000..ee96f4a91 --- /dev/null +++ b/frontends/server/mouse.lisp @@ -0,0 +1,15 @@ +(uiop:define-package :lem-server/mouse + (:use :cl) + (:export :update-position + :get-position)) +(in-package :lem-server/mouse) + +(defvar *mouse-x* 0) +(defvar *mouse-y* 0) + +(defun update-position (x y) + (setf *mouse-x* x + *mouse-y* y)) + +(defun get-position () + (values *mouse-x* *mouse-y*)) diff --git a/frontends/server/utils.lisp b/frontends/server/utils.lisp new file mode 100644 index 000000000..3aad793c3 --- /dev/null +++ b/frontends/server/utils.lisp @@ -0,0 +1,36 @@ +(defpackage :lem-server/utils + (:use :cl) + (:export :pdebug + :hash + :with-error-handler + :json-equal + :pretty-json)) +(in-package :lem-server/utils) + +(defun pdebug (fmt &rest args) + (apply #'format t fmt args) + (terpri)) + +(defun hash (&rest args) + (alexandria:plist-hash-table args :test #'equal)) + +(defmacro with-error-handler (() &body body) + `(handler-case + (handler-bind ((error (lambda (c) + (pdebug "~A" + (with-output-to-string (stream) + (format stream "~A~%" c) + (uiop:print-backtrace :stream stream + :condition c) + (force-output stream)))))) + ,@body) + (error ()))) + +(defun json-equal (x y) + (string= (with-output-to-string (out) (yason:encode x out)) + (with-output-to-string (out) (yason:encode y out)))) + +(defun pretty-json (value) + (with-output-to-string (out) + (yason:encode value + (yason:make-json-output-stream out)))) diff --git a/frontends/server/view.lisp b/frontends/server/view.lisp new file mode 100644 index 000000000..e4fba6d14 --- /dev/null +++ b/frontends/server/view.lisp @@ -0,0 +1,62 @@ +(defpackage :lem-server/view + (:use :cl) + (:export :view + :make-view + :view-window + :view-id + :view-x + :view-y + :view-width + :view-height + :view-use-modeline + :view-kind + :move-view + :resize-view)) +(in-package :lem-server/view) + +(defvar *view-id-counter* 0) + +(defstruct (view (:constructor %make-view)) + (id (incf *view-id-counter*)) + window + x + y + width + height + use-modeline + kind ; "tile" / "floating" + border) + +(defun make-view (&rest args &key window x y width height use-modeline kind border) + (declare (ignore window x y width height use-modeline kind border)) + (apply #'%make-view args)) + +(defun move-view (view x y) + (setf (view-x view) x + (view-y view) y)) + +(defun resize-view (view width height) + (setf (view-width view) width + (view-height view) height) + (values)) + +(defmethod yason:encode ((view view) &optional (stream *standard-output*)) + (yason:with-output (stream) + (yason:with-object () + (yason:encode-object-element "id" (view-id view)) + (yason:encode-object-element "x" (view-x view)) + (yason:encode-object-element "y" (view-y view)) + (yason:encode-object-element "width" (view-width view)) + (yason:encode-object-element "height" (view-height view)) + (yason:encode-object-element "use_modeline" (view-use-modeline view)) + (yason:encode-object-element "kind" (view-kind view)) + (yason:encode-object-element "type" + (let ((buffer (lem:window-buffer (view-window view)))) + (if (typep buffer 'lem:html-buffer) + "html" + "editor"))) + (yason:encode-object-element "content" + (let ((buffer (lem:window-buffer (view-window view)))) + (when (typep buffer 'lem:html-buffer) + (lem:html-buffer-html buffer)))) + (yason:encode-object-element "border" (view-border view))))) diff --git a/qlfile.lock b/qlfile.lock index 748150846..4311acd50 100644 --- a/qlfile.lock +++ b/qlfile.lock @@ -1,11 +1,11 @@ ("quicklisp" . (:class qlot/source/dist:source-dist :initargs (:distribution "https://beta.quicklisp.org/dist/quicklisp.txt" :%version :latest) - :version "2023-10-21")) + :version "2024-10-12")) ("micros" . (:class qlot/source/git:source-git :initargs (:remote-url "https://github.com/lem-project/micros.git") - :version "git-9fc7f1e5b0dbf1b9218a3f0aca7ed46e90aa86fd")) + :version "git-af94fe5d6688f67a092f604765fb706ebae44e99")) ("lem-mailbox" . (:class qlot/source/git:source-git :initargs (:remote-url "https://github.com/lem-project/lem-mailbox.git") @@ -29,7 +29,7 @@ ("cl-sdl2-ttf" . (:class qlot/source/git:source-git :initargs (:remote-url "https://github.com/lem-project/cl-sdl2-ttf.git") - :version "git-e61bb2119003d8ae7792d38aa11f7728d3ee5a00")) + :version "git-f43344efe89cf9ce509e6ce4f7303ebb2ff14434")) ("cl-sdl2-image" . (:class qlot/source/git:source-git :initargs (:remote-url "https://github.com/lem-project/cl-sdl2-image.git") @@ -37,7 +37,7 @@ ("jsonrpc" . (:class qlot/source/git:source-git :initargs (:remote-url "https://github.com/cxxxr/jsonrpc.git") - :version "git-a43dd933838bb9596a2bf40e821af0bafd3d5356")) + :version "git-2af1e0fad429ee8c706b86c4a853248cdd1be933")) ("dissect" . (:class qlot/source/git:source-git :initargs (:remote-url "https://github.com/Shinmera/dissect.git") diff --git a/scripts/build-server.lisp b/scripts/build-server.lisp new file mode 100644 index 000000000..4d24a8404 --- /dev/null +++ b/scripts/build-server.lisp @@ -0,0 +1,7 @@ +(ql:quickload :lem-server) + +(lem:init-at-build-time) + +(sb-ext:save-lisp-and-die "lem-server" + :toplevel #'lem-server:main + :executable t)