├── .gitignore ├── LICENSE ├── README.md ├── examples └── FilePicker │ ├── assets │ ├── favicon.ico │ ├── index.html │ └── main.js │ └── filepicker.nim ├── neel.nimble └── src ├── chrome.nim └── neel.nim /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.exe 3 | *.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Leon Lysak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neel | HTML/JS GUI Library for Nim 2 | 3 | Neel is a Nim library for making lightweight Electron-like HTML/JS GUI apps, with full access to Nim capabilities and targets any of the C, C++, or Objective-C backends. 4 | 5 | > By default, Neel opens a new Chrome session in app mode and allows the Nim backend and HTML/JS frontend to communicate via JSON and websockets. Alternate mode available, simply opening a new tab in your system's default browser. Webview capabilities coming soon. 6 | 7 | Neel is designed to take all the hassle out of writing GUI applications. Current Features: 8 | 9 | * Eliminate 99% of boilerplate code 10 | * Automatic routes 11 | * Embeds frontend assets at compiletime (on `release` builds) 12 | * Automatic type conversions (from JSON to each procedure's parameter types) 13 | * Simple interface for backend/frontend communication 14 | * Cross-platform (tested on Mac, Windows, and Linux) 15 | 16 | Neel is inspired by [Eel](https://github.com/samuelhwilliams/Eel), its Python cousin. 17 | 18 | ---------------------- 19 | 20 | ## Introduction 21 | 22 | Currently, Nim’s options for writing GUI applications are quite limited and if you wanted to use HTML/JS instead you can expect a lot of boilerplate code and headaches. 23 | 24 | Neel is still in its infancy, so as of right now I don’t think it’s suitable for making full-blown commercial applications like Slack or Twitch. It is, however, very suitable for making all kinds of other projects and tools. 25 | 26 | The best visualization libraries that exist are in Javascript and the most powerful capabilities of software can be harnessed with Nim. The goal of Neel is to combine the two languages and assist you in creating some killer applications. 27 | 28 | ## Installation 29 | 30 | Install from nimble: 31 | `nimble install neel` 32 | 33 | ## Usage 34 | 35 | ### Directory Structure 36 | 37 | Currently, Neel applications consist of various static web assets (HTML,CSS,JS, etc.) and Nim modules. 38 | 39 | All of the frontend files need to be placed within a single folder (they can be further divided into more folders within it if necessary). The starting URL must be named `index.html` and placed within the root of the web folder. 40 | 41 | ``` 42 | main.nim <----\ 43 | database.nim <-------- Nim files 44 | other.nim <----/ 45 | /assets/ <------- Web folder containing frontend files (can be named whatever you want) 46 | index.html <-------- The starting URL for the application (**must** be named 'index.html' and located within the root of the web folder) 47 | /css/ 48 | style.css 49 | /js/ 50 | main.js 51 | ``` 52 | 53 | ### Developing the Application 54 | 55 | #### Nim / Backend 56 | 57 | We begin with a very simple example, from there I'll explain the process and each part in detail. 58 | 59 | (main.nim) 60 | ```nim 61 | import neel #1 62 | 63 | exposeProcs: #2 64 | proc echoThis(jsMsg: string) = 65 | echo "got this from frontend: " & jsMsg 66 | callJs("logThis", "Hello from Nim!") #3 67 | 68 | startApp(webDirPath="path-to-web-assets-folder") #4 69 | ``` 70 | 71 | ##### #1 import neel 72 | 73 | When you `import neel`, several modules are exported into the calling module. `exposedProcs` and `startApp` are macros that require these modules in order to work properly. The list below are all of the exported modules. It's not necessary to remember them, and even if you accidently imported the module twice Nim disregards it. This is just for your reference really. 74 | * `std/os` 75 | * `std/osproc` 76 | * `std/strutils` 77 | * `std/json` 78 | * `std/threadpool` 79 | * `std/browsers` 80 | * `std/jsonutils` 81 | * `pkg/mummy` 82 | * `pkg/routers` 83 | 84 | ##### #2 exposeProcs 85 | 86 | `exposeProcs` is a macro that *exposes* specific procedures for javascript to be able to call from the frontend. When the macro is expanded, it creates a procedure `callNim` which contains **all exposed procedures** and will call a specified procedure based on frontend data, passing in the appropriate parameters (should there be any). 87 | 88 | The data being received is initially **JSON** and needs to be converted into the appropriate types for each parameter in a procedure. This is also handled by the macro. 89 | 90 | As of Neel 1.1.0, you can use virtually any Nim type for parameters in *exposed procedures*. Neel uses `std/jsonutils` to programmatically handle the conversions. Some caveats: 91 | * Does not support default values for parameters. 92 | * Does not support generics for parameters. 93 | 94 | This above macro produces this result: 95 | 96 | ```nim 97 | proc callNim(procName: string; params: seq[JsonNode]) = 98 | proc echoThis(jsMsg: string) = 99 | echo "got this from frontend: " & jsMsg 100 | callJs("logThis", "Hello from Nim!") 101 | case procName 102 | of "echoThis": echoThis(params[0].getStr) 103 | ... 104 | ``` 105 | 106 | NOTE: You *can* pass complex data in a single parameter now if you'd like to. Use Javascript objects or dictionaries and simply create a custom object type to accept it from the Nim side. 107 | 108 | I'm sure this is obvious, but it's much cleaner to have your exposed procedures call procedures from other modules. 109 | 110 | Example: 111 | ```nim 112 | # (main.nim) 113 | import neel, othermodule 114 | exposeProcs: 115 | proc proc1(param: seq[JsonNode]) = 116 | doStuff(param[0].getInt) 117 | ... 118 | 119 | # (othermodule.nim) 120 | from neel import callJs # you only need to import this macro from Neel :) 121 | 122 | proc doStuff*(param: int) = 123 | var dataForFrontEnd = param + 100 124 | callJs("myJavascriptFunc", dataForFrontEnd) 125 | ... 126 | ``` 127 | 128 | ##### #3 callJs 129 | 130 | `callJs` is a macro that takes in at least one value, a `string`, and it's *the name of the javascript function you want to call*. Any other value will be passed into that javascript function call on the frontend. You may pass in any amount to satisfy your function parameters needs like so: 131 | 132 | ```nim 133 | callJs("myJavascriptFunc",1,3.14,["some stuff",1,9000]) 134 | ``` 135 | 136 | The above code gets converted into stringified JSON and sent to the frontend via websocket. 137 | 138 | ##### #4 startApp 139 | 140 | `startApp` is a macro that handles server logic, routing, and Chrome web browser. `startApp` currently takes 6 parameters. 141 | example: 142 | 143 | ```nim 144 | startApp(webDirPath= "path_to_web_assets_folder", portNo=5000, 145 | position= [500,150], size= [600,600], chromeFlags= @["--force-dark-mode"], appMode= true) 146 | # left, top # width, height 147 | ``` 148 | 149 | * `webDirPath` : sets the path to the web directory containing all frontend assets **needs to be set** 150 | * `portNo` : specifies the port for serving your application (default is 5000) 151 | * `position` : positions the *top* and *left* side of your application window (default is 500 x 150) 152 | * `size` : sets the size of your application window by *width* and *height*(default is 600 x 600) 153 | * `chromeFlags` : passes any additional flags to chrome (default is none) 154 | * `appMode` : if "true" (default) Chrome will open a new session/window in App mode, if "false" a new tab will be opened in your *current default browser* - which can be very useful for debugging. 155 | 156 | #### Javascript / Frontend 157 | 158 | The Javascript aspect of a Neel app isn't nearly as complex. Let's build the frontend for the example above: 159 | 160 | (index.html) 161 | ```html 162 | 163 | 164 | 165 | 166 | 167 | Neel App Example 168 | 169 | 170 | 171 |

My First Neel App

172 | 173 | 174 | 175 | ``` 176 | (main.js) 177 | ```javascript 178 | neel.callNim("echoThis","Hello from Javascript!") 179 | 180 | function logThis(param){ 181 | console.log(param) 182 | } 183 | ``` 184 | The first thing you'll notice is we've included a script tag containing `/neel.js` in the section of our HTML page. This allows Neel to handle all of the logic on the frontend for websocket connections and function/procedure calls. 185 | 186 | `neel.callNim` is a function that takes in at least one value, a `string`, and it's *the name of the Nim procedure you want to call*. Any other value will be passed into that Nim procedure call on the backend asa parameter. **You must pass in the correct number of params for that proc, in order, and of the correct types for it to be called properly.** Example: 187 | 188 | frontend call to backend: 189 | ```javascript 190 | neel.callNim("myNimProc",1,3.14,["some stuff",1,9000]) 191 | ``` 192 | must match the result of the `exposeProcs` macro: 193 | ```nim 194 | ... 195 | of "myNimProc": return myNimProc(params[0].getInt,params[1].getFloat,params[2].getElems) 196 | ... 197 | ``` 198 | 199 | As of Neel 1.1.0, there is exception handling in place (with a verbose logging in debug builds) for unknown procedure calls and parameter type mismatches. 200 | 201 | Going back to our first example, when `index.html` is served, Javascript will call the `echoThis` procedure and pass "Hello from Javascript!" as the param. This will print the string in the terminal. Then, Nim will call the `logThis` function and pass "Hello from Nim!". Neel handles the JSON conversion, calls the function and passes in the param. 202 | 203 | Now open the console in Chrome developer tools and you should see "Hello from Nim!". 204 | 205 | **Keep In Mind: absolute paths must be used within your HTML files** ex: ` 9 | 10 | 11 | 12 | 13 | 14 | 15 |

16 | 17 | -------------------------------------------------------------------------------- /examples/FilePicker/assets/main.js: -------------------------------------------------------------------------------- 1 | function sendDir() { 2 | const directory = document.getElementById("directory").value 3 | neel.callNim("filePicker", directory) 4 | } 5 | 6 | function showText(fileName) { 7 | document.getElementById("fileName").innerHTML = fileName 8 | } -------------------------------------------------------------------------------- /examples/FilePicker/filepicker.nim: -------------------------------------------------------------------------------- 1 | # compile with: --threads:on and --gc:orc if using Nim 1.6.X 2 | # add --app:gui if on Windows to get prevent terminal from opening (though useful for debugging) 3 | 4 | import std/[os, random] 5 | import neel 6 | 7 | randomize() 8 | 9 | exposeProcs: 10 | proc filePicker(directory: string) = 11 | let absPathDir = absolutePath(directory,root=getHomeDir()) 12 | if dirExists(absPathDir): 13 | var files: seq[string] 14 | for kind, path in absPathDir.walkDir: 15 | files.add(path) 16 | callJs("showText",sample(files)) 17 | else: 18 | callJs("showText", "The directory you chose does not exist.") 19 | 20 | startApp(webDirPath= currentSourcePath.parentDir / "assets") # change path as necessary -------------------------------------------------------------------------------- /neel.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "1.1.0" 4 | author = "Leon Lysak, Blane Lysak" 5 | description = "A Nim library for making lightweight Electron-like HTML/JS GUI apps, with full access to Nim capabilities." 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | # Dependencies 10 | 11 | requires "nim >= 1.6.8" 12 | requires "mummy >= 0.3.4" -------------------------------------------------------------------------------- /src/chrome.nim: -------------------------------------------------------------------------------- 1 | import std/[strutils, os, sequtils, osproc] 2 | 3 | type BrowserNotFound = object of CatchableError 4 | 5 | proc findChromeMac: string = 6 | const defaultPath :string = r"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" 7 | const name = "Google Chrome.app" 8 | 9 | try: 10 | if fileExists(absolutePath(defaultPath)): 11 | result = defaultPath.replace(" ", r"\ ") 12 | else: 13 | var alternateDirs = execProcess("mdfind", args = [name], options = {poUsePath}).split("\n") 14 | alternateDirs.keepItIf(it.contains(name)) 15 | 16 | if alternateDirs != @[]: 17 | result = alternateDirs[0] & "/Contents/MacOS/Google Chrome" 18 | else: 19 | raise newException(BrowserNotFound, "could not find Chrome using `mdfind`") 20 | 21 | except: 22 | raise newException(BrowserNotFound, "could not find Chrome in Applications directory") 23 | 24 | 25 | when defined(Windows): 26 | import std/registry 27 | 28 | proc findChromeWindows: string = 29 | const defaultPath = r"\Program Files (x86)\Google\Chrome\Application\chrome.exe" 30 | const backupPath = r"\Program Files\Google\Chrome\Application\chrome.exe" 31 | if fileExists(absolutePath(defaultPath)): 32 | result = defaultPath 33 | elif fileExists(absolutePath(backupPath)): 34 | result = backupPath 35 | else: 36 | when defined(Windows): 37 | result = getUnicodeValue( 38 | path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe", 39 | key = "", handle = HKEY_LOCAL_MACHINE) 40 | discard 41 | 42 | if result.len == 0: 43 | raise newException(BrowserNotFound, "could not find Chrome") 44 | 45 | 46 | proc findChromeLinux: string = 47 | const chromeNames = ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"] 48 | for name in chromeNames: 49 | if execCmd("which " & name) == 0: 50 | return name 51 | raise newException(BrowserNotFound, "could not find Chrome") 52 | 53 | 54 | proc findPath: string = 55 | when hostOS == "macosx": 56 | result = findChromeMac() 57 | elif hostOS == "windows": 58 | result = findChromeWindows() 59 | elif hostOS == "linux": 60 | result = findChromeLinux() 61 | else: 62 | raise newException(BrowserNotFound, "unkown OS in findPath() for neel.nim") 63 | 64 | 65 | proc openChrome*(portNo: int, chromeFlags: seq[string]) = 66 | var command = " --app=http://localhost:" & portNo.intToStr & "/ --disable-http-cache" 67 | if chromeFlags != @[""]: 68 | for flag in chromeFlags: 69 | command = command & " " & flag.strip 70 | let finalCommand = findPath() & command 71 | if execCmd(finalCommand) != 0: 72 | raise newException(BrowserNotFound, "could not open Chrome browser") -------------------------------------------------------------------------------- /src/neel.nim: -------------------------------------------------------------------------------- 1 | import std/[macros, os, strutils, osproc, json, threadpool, browsers, uri, tables, jsonutils] 2 | import pkg/[mummy, mummy/routers] 3 | import chrome 4 | export os, strutils, osproc, json, threadpool, browsers, jsonutils, mummy, routers 5 | 6 | 7 | # ------------ EMBED STATIC ASSETS LOGIC ----------------------------- 8 | 9 | proc getWebFolder*(webDirPath: string): Table[string, string] {.compileTime.} = 10 | for path in walkDirRec(webDirPath,relative=true): 11 | if path == "index.html": 12 | result["/"] = staticRead(webDirPath / path) 13 | else: 14 | result["/" & path.replace('\\','/')] = staticRead(webDirPath / path) 15 | 16 | # ---------------------------------------------------------------------- 17 | 18 | 19 | # ------------- TRANSFORMATIONS & TYPE CONVERSION LOGIC ---------------- 20 | 21 | var frontendSocket*: WebSocket # 22 | 23 | macro callJs*(funcName: string, params: varargs[untyped]) = 24 | quote do: 25 | frontendSocket.send($(%*{"funcName":`funcName`,"params":[`params`]})) 26 | 27 | proc validation*(procs: NimNode) = 28 | 29 | for procedure in procs.children: 30 | 31 | procedure.expectKind(nnkProcDef) #each has to be a proc definition 32 | procedure[3][0].expectKind(nnkEmpty) #there should be no return type 33 | procedure[4].expectKind(nnkEmpty) #there should be no pragma 34 | 35 | for param in procedure.params: 36 | if param.kind != nnkEmpty: 37 | param[2].expectKind(nnkEmpty) # default value for parameters should be empty 38 | 39 | proc ofStatements*(procedure: NimNode): NimNode = 40 | 41 | if procedure[3].len == 1:#handles procedure w/ empty params (formalParams has one child of kind nnkEmpty) 42 | result = nnkOfBranch.newTree( 43 | newLit(procedure[0].repr), #name of proc 44 | nnkStmtList.newTree( 45 | nnkCall.newTree( 46 | newIdentNode(procedure[0].repr) #name of proc 47 | ))) 48 | else: 49 | result = nnkOfBranch.newTree() 50 | result.add newLit(procedure[0].repr) #name of proc 51 | var 52 | statementList = nnkStmtList.newTree() 53 | procCall = nnkCall.newTree() 54 | procCall.add newIdentNode(procedure[0].repr) #name of proc 55 | 56 | var idx = 0 57 | for param in procedure.params: 58 | if param.kind == nnkEmpty: continue # the first argument here is return type for proc (which must be empty) 59 | let paramType = param[1] 60 | procCall.add( 61 | nnkCall.newTree(nnkDotExpr.newTree( 62 | nnkBracketExpr.newTree( 63 | newIdentNode("params"),newLit(idx)),newIdentNode("jsonTo")), 64 | paramType)) 65 | idx += 1 66 | 67 | statementList.add procCall 68 | result.add statementList 69 | 70 | proc caseStatement*(procs: NimNode): NimNode = 71 | 72 | result = nnkCaseStmt.newTree() 73 | result.add newIdentNode("procName") 74 | 75 | for procedure in procs: 76 | result.add ofStatements(procedure) #converts proc param types for parsing json data & returns "of" statements 77 | 78 | result.add nnkElse.newTree( # else statement for handling unknown procedure call from frontend 79 | nnkStmtList.newTree( 80 | nnkWhenStmt.newTree( 81 | nnkElifBranch.newTree( 82 | nnkPrefix.newTree( 83 | newIdentNode("not"), 84 | nnkCall.newTree( 85 | newIdentNode("defined"), 86 | newIdentNode("release"))), 87 | nnkStmtList.newTree( 88 | nnkCommand.newTree( 89 | newIdentNode("echo"), 90 | nnkInfix.newTree( 91 | newIdentNode("&"), 92 | newLit("Uknown procedure called from frontend: "), 93 | newIdentNode("procName"))))), 94 | nnkElse.newTree( 95 | nnkStmtList.newTree( 96 | nnkDiscardStmt.newTree( 97 | newEmptyNode())))))) 98 | 99 | macro exposeProcs*(procs: untyped) = #macro has to be untyped, otherwise callJs() expands & causes a type error 100 | procs.validation() #validates procs passed into the macro 101 | 102 | result = nnkProcDef.newTree( 103 | newIdentNode("callNim"), 104 | newEmptyNode(), 105 | newEmptyNode(), 106 | nnkFormalParams.newTree( 107 | newEmptyNode(), 108 | nnkIdentDefs.newTree(newIdentNode("procName"), newIdentNode("string"), newEmptyNode()), 109 | nnkIdentDefs.newTree(newIdentNode("params"), nnkBracketExpr.newTree(newIdentNode("seq"),newIdentNode("JsonNode")), newEmptyNode()) 110 | ), 111 | newEmptyNode(), 112 | newEmptyNode(), 113 | nnkStmtList.newTree( 114 | procs, 115 | caseStatement(procs) # converts types into proper json parsing & returns case statement logic 116 | ) 117 | ) 118 | 119 | # echo result.repr # for testing macro expansion 120 | 121 | # ---------------------------------------------------------------------- 122 | 123 | # ---------------------------- SERVER LOGIC ---------------------------- 124 | 125 | macro startApp*(webDirPath: string, portNo: int = 5000, 126 | position: array[2, int] = [500,150], size: array[2, int] = [600,600], 127 | chromeFlags: seq[string] = @[""], appMode: bool = true) = 128 | quote do: 129 | when defined(release): 130 | const Assets = getWebFolder(`webDirPath`) 131 | var 132 | openSockets: bool 133 | server: Server 134 | 135 | proc handleFrontEndData*(frontEndData :string) {.gcsafe.} = 136 | let 137 | frontendDataJson = frontendData.parseJson 138 | procName = frontendDataJson["procName"].getStr 139 | params = frontendDataJson["params"].getElems 140 | try: 141 | callNim(procName, params) 142 | except: 143 | when not defined(release): 144 | echo "\nError from Javascript call to Nim.\nFunction: " & procName & "\nParameters: " & $params 145 | echo "ERROR [" & $(getCurrentException().name) & "] Message: " & getCurrentExceptionMsg() 146 | else: discard 147 | 148 | proc shutdown = # quick implementation to reduce crashing from users spamming refresh / clicking on new pages 149 | const maxTime = when defined(release): 10 else: 3 # update: long delay applies to release builds 150 | for i in 1 .. maxTime: 151 | sleep 1000 152 | if openSockets: return 153 | when not defined(release): echo "Trying to re-establish a connection: " & $i & "/" & $maxTime 154 | if not openSockets: 155 | when not defined(release): echo "Shutting down." 156 | server.close() 157 | # quit() 158 | 159 | if `appMode`: 160 | spawn openChrome(portNo=`portNo`, chromeFlags=`chromeFlags`) 161 | else: 162 | spawn openDefaultBrowser("http://localhost:" & $`portNo` & "/") 163 | 164 | proc indexHandler(request: Request) = 165 | var headers: HttpHeaders 166 | headers["Cache-Control"] = "no-store" 167 | when not defined(release): 168 | request.respond(200, headers, readFile(`webDirPath` / "index.html")) 169 | else: 170 | let path = parseUri(request.uri).path 171 | request.respond(200, headers, Assets[path]) 172 | proc jsHandler(request: Request) = 173 | var headers: HttpHeaders 174 | headers["Cache-Control"] = "no-store" 175 | headers["Content-Type"] = "application/javascript" 176 | request.respond(200, headers,"window.moveTo(" & $`position`[0] & "," & $`position`[1] & ")\n" & 177 | "window.resizeTo(" & $`size`[0] & "," & $`size`[1] & ")\n" & 178 | "var ws = new WebSocket(\"ws://localhost:" & $`portNo` & "/ws\")\n" & 179 | """var connected = false 180 | ws.onmessage = (data) => { 181 | let x; 182 | try { 183 | x = JSON.parse(data.data) 184 | } catch (err) { 185 | x = JSON.parse(data) 186 | } 187 | let v = Object.values(x) 188 | neel.callJs(v[0], v[1]) 189 | } 190 | var neel = { 191 | callJs: function (func, arr) { 192 | window[func].apply(null, arr); 193 | }, 194 | callNim: function (func, ...args) { 195 | if (!connected) { 196 | function check(func, ...args) { if (ws.readyState == 1) { connected = true; neel.callNim(func, ...args); clearInterval(myInterval); } } 197 | var myInterval = setInterval(check,15,func,...args) 198 | } else { 199 | let paramArray = [] 200 | for (var i = 0; i < args.length; i++) { 201 | paramArray.push(args[i]) 202 | } 203 | let data = JSON.stringify({ "procName": func, "params": paramArray }) 204 | ws.send(data) 205 | } 206 | } 207 | }""") 208 | 209 | proc wsHandler(request: Request) = 210 | let ws = request.upgradeToWebSocket() 211 | frontendSocket = ws 212 | 213 | proc websocketHandler(websocket: WebSocket, event: WebSocketEvent, message: Message) = 214 | case event: 215 | of OpenEvent: 216 | when not defined(release): 217 | echo "App opened connection." 218 | openSockets = true 219 | of MessageEvent: 220 | spawn handleFrontEndData(message.data) 221 | of ErrorEvent: 222 | when not defined(release): 223 | echo "Socket error: ", message 224 | else: discard 225 | # spawn shutdown() # 11/1/23: I don't think we need to spawn a shutdown. 226 | of CloseEvent: 227 | when not defined(release): 228 | echo "Socket closed." 229 | openSockets = false 230 | spawn shutdown() 231 | 232 | proc pathHandler(request: Request) = 233 | let path = parseUri(request.uri).path 234 | try: 235 | var headers: HttpHeaders 236 | headers["Cache-Control"] = "no-store" 237 | if "js" == path.split('.')[^1]: # forcing MIME-type to support JS modules 238 | headers["Content-Type"] = "application/javascript" 239 | when not defined(release): 240 | request.respond(200, headers, readFile(`webDirPath` / path)) 241 | else: 242 | request.respond(200,headers,Assets[path]) 243 | except: 244 | raise newException(ValueError, "path: " & path & " doesn't exist") 245 | 246 | var router: Router 247 | router.get("/", indexHandler) 248 | router.get("/neel.js", jsHandler) 249 | router.get("/ws", wsHandler) 250 | router.get("/**", pathHandler) 251 | server = newServer(router, websocketHandler) 252 | server.serve(Port(`portNo`)) --------------------------------------------------------------------------------