├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── bootstrap.bat ├── bootstrap.sh ├── deps.ts ├── examples ├── custom_file_handler │ ├── assets │ │ ├── test_app.js │ │ └── webui.jpeg │ ├── custom_file_handler.ts │ └── index.html ├── custom_web_server │ ├── custom_web_server.ts │ ├── index.html │ ├── second.html │ └── simple_web_server.py ├── hello_world │ └── hello_world.ts └── send_raw_binary │ ├── send_raw_binary.ts │ └── webui.jpeg ├── img ├── cppcon_2019.png ├── screenshot.png ├── webui.png ├── webui_bun_example.png └── webui_diagram.png ├── mod.ts ├── package-lock.json ├── package.json └── src ├── bootstrap.bat ├── bootstrap.sh ├── ffi_worker.ts ├── lib.ts ├── types.ts ├── utils.ts └── webui.ts /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '18' 21 | registry-url: 'https://registry.npmjs.org/' 22 | 23 | - name: Install dependencies 24 | run: echo "Bun-WebUI has no dependencies for now." 25 | 26 | - name: Publish to NPM 27 | run: npm publish --access public 28 | env: 29 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Bun binaries 2 | src/webui-windows-msvc-x64/ 3 | src/webui-windows-msvc-arm/ 4 | src/webui-windows-msvc-arm64/ 5 | src/webui-macos-clang-x64/ 6 | src/webui-macos-clang-arm/ 7 | src/webui-macos-clang-arm64/ 8 | src/webui-linux-gcc-x64/ 9 | src/webui-linux-gcc-arm/ 10 | src/webui-linux-gcc-arm64/ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Archives 16 | *.zip 17 | *.tar 18 | *.gz 19 | 20 | # Build 21 | *.exe 22 | *.ilk 23 | *.pdb 24 | *.exp 25 | *.res 26 | *.out 27 | *.def 28 | *.obj 29 | *.iobj 30 | *.o 31 | 32 | # Logs 33 | *.log 34 | *.logs 35 | *.tlog 36 | 37 | # IDE 38 | .vscode/ 39 | .vs/ 40 | 41 | # Visual Studio 42 | .idea/ 43 | *.recipe 44 | *.idb 45 | 46 | # Visual Studio for Mac 47 | .idea/ 48 | 49 | # Visual Studio cache files 50 | ipch/ 51 | *.dbmdl 52 | *.dbproj.schemaview 53 | 54 | # Others 55 | .builds 56 | *~* 57 | *.cache 58 | *.swp 59 | *.bak 60 | *.tmp 61 | *.swp 62 | *.userosscache 63 | *.err 64 | *.vspscc 65 | *.vssscc 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # NuGet 71 | packages/ 72 | !packages/repositories.config 73 | *.nupkg 74 | 75 | # Microsoft Azure Build Output 76 | csx/ 77 | *.build.csdef 78 | 79 | # User-specific files 80 | *.suo 81 | *.user 82 | *.userprefs 83 | *.sln.docstates 84 | 85 | # Python 86 | __pycache__/ 87 | dist/ 88 | webui2.egg-info/ 89 | 90 | # Rust 91 | target/ 92 | *.lock 93 | 94 | # Broken NTFS 95 | nul 96 | 97 | # Zig 98 | zig-cache/ 99 | zig-out/ 100 | 101 | # macOS 102 | .DS_Store 103 | .DS_Store? 104 | ._* 105 | .Spotlight-V100 106 | .Trashes 107 | ehthumbs.db 108 | Thumbs.db 109 | 110 | # User-specific private settings 111 | *.DotSettings.user 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Hassan Draga 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 |
2 | 3 | ![Logo](https://raw.githubusercontent.com/webui-dev/webui-logo/main/webui_bun.png) 4 | 5 | # Bun-WebUI v2.5.2 (Beta) 6 | 7 | [last-commit]: https://img.shields.io/github/last-commit/webui-dev/webui?style=for-the-badge&logo=github&logoColor=C0CAF5&labelColor=414868 8 | [release-version]: https://img.shields.io/github/v/tag/webui-dev/webui?style=for-the-badge&logo=webtrees&logoColor=C0CAF5&labelColor=414868&color=7664C6 9 | [license]: https://img.shields.io/github/license/webui-dev/webui?style=for-the-badge&logo=opensourcehardware&label=License&logoColor=C0CAF5&labelColor=414868&color=8c73cc 10 | 11 | [![][last-commit]](https://github.com/webui-dev/bun-webui/pulse) 12 | [![][release-version]](https://github.com/webui-dev/bun-webui/releases/latest) 13 | [![][license]](https://github.com/webui-dev/bun-webui/blob/main/LICENSE) 14 | 15 | > Use any web browser or WebView as GUI, with Bun in the backend and modern web technologies in the frontend, all in a lightweight portable library. 16 | 17 | ![Screenshot](https://raw.githubusercontent.com/webui-dev/webui-logo/main/screenshot.png) 18 | 19 |
20 | 21 | ## Features 22 | 23 | - Portable (*Needs only a web browser or a WebView at runtime*) 24 | - Lightweight (*Few Kb library*) & Small memory footprint 25 | - Fast binary communication protocol 26 | - Multi-platform & Multi-Browser 27 | - Using private profile for safety 28 | - Cross-platform WebView 29 | 30 | ## Screenshot 31 | 32 | This [hello world example](https://github.com/webui-dev/bun-webui/tree/main/examples/hello_world) 33 | is written in Bun using WebUI as the GUI library. 34 | 35 | ![ScreenShot](img/webui_bun_example.png) 36 | 37 | ## NPM Installation 38 | 39 | ```sh 40 | npm install @webui-dev/bun-webui 41 | ``` 42 | 43 | ## Import Package 44 | 45 | ```js 46 | import { WebUI } from '@webui-dev/bun-webui'; 47 | ``` 48 | 49 | ## Minimal Example 50 | 51 | ```js 52 | import { WebUI } from '@webui-dev/bun-webui'; 53 | 54 | const myWindow = new WebUI(); 55 | myWindow.show(' Hello World! '); 56 | await WebUI.wait(); 57 | ``` 58 | 59 | ```sh 60 | bun run minimal.ts 61 | ``` 62 | 63 | [More examples](https://github.com/webui-dev/bun-webui/tree/main/examples) 64 | 65 | ## Documentation 66 | 67 | - [Online Documentation](https://webui.me/docs/2.5/#/) (_Deno section is the same as Bun_) 68 | 69 | ## CppCon 2019 Presentation 70 | 71 | [Borislav Stanimirov](https://ibob.bg/) explained at 72 | [C++ Conference 2019 (_YouTube_)](https://www.youtube.com/watch?v=bbbcZd4cuxg) 73 | how beneficial it is to use the web browser as GUI. 74 | 75 | 78 | 79 | ![ScreenShot](img/cppcon_2019.png) 80 | 81 | ## UI & The Web Technologies 82 | 83 | Web application UI design is not just about how a product looks but how it 84 | works. Using web technologies in your UI makes your product modern and 85 | professional, And a well-designed web application will help you make a solid 86 | first impression on potential customers. Great web application design also 87 | assists you in nurturing leads and increasing conversions. In addition, it makes 88 | navigating and using your web app easier for your users. 89 | 90 | ## Why Use Web Browser? 91 | 92 | Today's web browsers have everything a modern UI needs. Web browsers are very 93 | sophisticated and optimized. Therefore, using it as a GUI will be an excellent 94 | choice. While old legacy GUI lib is complex and outdated, a WebView-based app is 95 | still an option. However, a WebView needs a huge SDK to build and many 96 | dependencies to run, and it can only provide some features like a real web 97 | browser. That is why WebUI uses real web browsers to give you full features of 98 | comprehensive web technologies while keeping your software lightweight and 99 | portable. 100 | 101 | ## How does it work? 102 | 103 | ![ScreenShot](img/webui_diagram.png) 104 | 105 | Think of WebUI like a WebView controller, but instead of embedding the WebView 106 | controller in your program, which makes the final program big in size, and 107 | non-portable as it needs the WebView runtimes. Instead, by using WebUI, you use 108 | a tiny static/dynamic library to run any installed web browser and use it as 109 | GUI, which makes your program small, fast, and portable. **All it needs is a web 110 | browser**. 111 | 112 | ## Runtime Dependencies Comparison 113 | 114 | | | Tauri / WebView | Qt | WebUI | 115 | | ------------------------------- | ----------------- | -------------------------- | ------------------- | 116 | | Runtime Dependencies on Windows | _WebView2_ | _QtCore, QtGui, QtWidgets_ | **_A Web Browser_** | 117 | | Runtime Dependencies on Linux | _GTK3, WebKitGTK_ | _QtCore, QtGui, QtWidgets_ | **_A Web Browser_** | 118 | | Runtime Dependencies on macOS | _Cocoa, WebKit_ | _QtCore, QtGui, QtWidgets_ | **_A Web Browser_** | 119 | 120 | ## Supported Web Browsers 121 | 122 | | Browser | Windows | macOS | Linux | 123 | | --------------- | --------------- | ------------- | --------------- | 124 | | Mozilla Firefox | ✔️ | ✔️ | ✔️ | 125 | | Google Chrome | ✔️ | ✔️ | ✔️ | 126 | | Microsoft Edge | ✔️ | ✔️ | ✔️ | 127 | | Chromium | ✔️ | ✔️ | ✔️ | 128 | | Yandex | ✔️ | ✔️ | ✔️ | 129 | | Brave | ✔️ | ✔️ | ✔️ | 130 | | Vivaldi | ✔️ | ✔️ | ✔️ | 131 | | Epic | ✔️ | ✔️ | _not available_ | 132 | | Apple Safari | _not available_ | _coming soon_ | _not available_ | 133 | | Opera | _coming soon_ | _coming soon_ | _coming soon_ | 134 | 135 | ## Supported Languages 136 | 137 | | Language | v2.4.0 API | v2.5.0 API | Link | 138 | | --------------- | --- | -------------- | --------------------------------------------------------- | 139 | | Python | ✔️ | _not complete_ | [Python-WebUI](https://github.com/webui-dev/python-webui) | 140 | | Go | ✔️ | _not complete_ | [Go-WebUI](https://github.com/webui-dev/go-webui) | 141 | | Zig | ✔️ | _not complete_ | [Zig-WebUI](https://github.com/webui-dev/zig-webui) | 142 | | Nim | ✔️ | _not complete_ | [Nim-WebUI](https://github.com/webui-dev/nim-webui) | 143 | | V | ✔️ | _not complete_ | [V-WebUI](https://github.com/webui-dev/v-webui) | 144 | | Rust | _not complete_ | _not complete_ | [Rust-WebUI](https://github.com/webui-dev/rust-webui) | 145 | | TS / JS (Deno) | ✔️ | _not complete_ | [Deno-WebUI](https://github.com/webui-dev/bun-webui) | 146 | | TS / JS (Bun) | _not complete_ | _not complete_ | [Bun-WebUI](https://github.com/webui-dev/bun-webui) | 147 | | Swift | _not complete_ | _not complete_ | [Swift-WebUI](https://github.com/webui-dev/swift-webui) | 148 | | Odin | _not complete_ | _not complete_ | [Odin-WebUI](https://github.com/webui-dev/odin-webui) | 149 | | Pascal | _not complete_ | _not complete_ | [Pascal-WebUI](https://github.com/webui-dev/pascal-webui) | 150 | | Purebasic | _not complete_ | _not complete_ | [Purebasic-WebUI](https://github.com/webui-dev/purebasic-webui)| 151 | | - | | | 152 | | Common Lisp | _not complete_ | _not complete_ | [cl-webui](https://github.com/garlic0x1/cl-webui) | 153 | | Delphi | _not complete_ | _not complete_ | [WebUI4Delphi](https://github.com/salvadordf/WebUI4Delphi) | 154 | | C# | _not complete_ | _not complete_ | [WebUI4CSharp](https://github.com/salvadordf/WebUI4CSharp) | 155 | | WebUI.NET | _not complete_ | _not complete_ | [WebUI.NET](https://github.com/Juff-Ma/WebUI.NET) | 156 | | QuickJS | _not complete_ | _not complete_ | [QuickUI](https://github.com/xland/QuickUI) | 157 | | PHP | _not complete_ | _not complete_ | [PHPWebUiComposer](https://github.com/KingBes/php-webui-composer) | 158 | 159 | ## Supported WebView 160 | 161 | | WebView | Status | 162 | | --------------- | --------------- | 163 | | Windows WebView2 | ✔️ | 164 | | Linux GTK WebView | ✔️ | 165 | | macOS WKWebView | ✔️ | 166 | 167 | ### License 168 | 169 | > Licensed under MIT License. 170 | 171 | ### Stargazers 172 | 173 | [![Stargazers repo roster for @webui-dev/bun-webui](https://reporoster.com/stars/webui-dev/bun-webui)](https://github.com/webui-dev/bun-webui/stargazers) 174 | -------------------------------------------------------------------------------- /bootstrap.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | cd src 3 | call bootstrap.bat %* 4 | cd .. 5 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd src 3 | sh bootstrap.sh "$@" 4 | cd .. 5 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | // Bun WebUI 2 | // Dependences needed by webui.ts 3 | 4 | import { 5 | fileExists, 6 | downloadCoreLibrary, 7 | currentModulePath, 8 | } from "./src/utils.ts"; 9 | 10 | /** 11 | * Determines the correct library filename based on the current operating system and CPU architecture. 12 | * Checks for the library locally and, if not found, constructs the full path and downloads it if necessary. 13 | * 14 | * @returns A promise that resolves to the path of the dynamic library. 15 | */ 16 | async function getLibName() { 17 | let fileName = ""; 18 | let localFileName = ""; 19 | 20 | // Select the appropriate library file based on platform and architecture. 21 | switch (process.platform) { 22 | // Windows platform 23 | case "win32": 24 | switch (process.arch) { 25 | case "x64": 26 | fileName = "webui-windows-msvc-x64/webui-2.dll"; 27 | localFileName = "./webui-2.dll"; 28 | break; 29 | case "arm64": 30 | fileName = "webui-windows-msvc-arm64/webui-2.dll"; 31 | localFileName = "./webui-2.dll"; 32 | break; 33 | default: 34 | throw new Error(`Unsupported architecture ${process.arch} for Windows`); 35 | } 36 | break; 37 | 38 | // macOS platform 39 | case "darwin": 40 | switch (process.arch) { 41 | case "x64": 42 | fileName = "webui-macos-clang-x64/libwebui-2.dylib"; 43 | localFileName = "./libwebui-2.dylib"; 44 | break; 45 | case "arm64": 46 | fileName = "webui-macos-clang-arm64/libwebui-2.dylib"; 47 | localFileName = "./libwebui-2.dylib"; 48 | break; 49 | default: 50 | throw new Error(`Unsupported architecture ${process.arch} for macOS`); 51 | } 52 | break; 53 | 54 | // Other platforms (Linux, etc.) 55 | default: 56 | switch (process.arch) { 57 | case "x64": 58 | fileName = "webui-linux-gcc-x64/libwebui-2.so"; 59 | localFileName = "./libwebui-2.so"; 60 | break; 61 | case "arm64": 62 | fileName = "webui-linux-gcc-arm64/libwebui-2.so"; 63 | localFileName = "./libwebui-2.so"; 64 | break; 65 | default: 66 | throw new Error(`Unsupported architecture ${process.arch} for ${process.platform}`); 67 | } 68 | break; 69 | } 70 | 71 | // Check if the dynamic library already exists locally. 72 | const localExists = await fileExists(localFileName); 73 | if (localExists) { 74 | return localFileName; 75 | } 76 | 77 | // Construct the full path to the dynamic library using the current module path. 78 | const srcFullPath = currentModulePath; 79 | const fullPath = srcFullPath + fileName; 80 | 81 | // Verify if the library exists at the computed path. 82 | const exists = await fileExists(fullPath); 83 | if (!exists) { 84 | // If not found, download the core library. 85 | await downloadCoreLibrary(); 86 | } 87 | 88 | // Return the final resolved path to the dynamic library. 89 | return fullPath; 90 | } 91 | 92 | // Export the resolved library name for use in other parts of the application. 93 | export const libName = await getLibName(); 94 | -------------------------------------------------------------------------------- /examples/custom_file_handler/assets/test_app.js: -------------------------------------------------------------------------------- 1 | 2 | function test_app() { 3 | alert('Hello from test_app.js'); 4 | } 5 | -------------------------------------------------------------------------------- /examples/custom_file_handler/assets/webui.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webui-dev/bun-webui/bf16e4f9413f16dce2b87dfed11dfe0fec91af73/examples/custom_file_handler/assets/webui.jpeg -------------------------------------------------------------------------------- /examples/custom_file_handler/custom_file_handler.ts: -------------------------------------------------------------------------------- 1 | // To run this script: 2 | // bun run custom_file_handler.ts 3 | 4 | // To import from local (Debugging and Development) 5 | // import { WebUI } from "../../mod.ts"; 6 | 7 | // To import from NPM (Production) 8 | import { WebUI } from '@webui-dev/bun-webui'; 9 | 10 | // Return HTTP header + file raw binary content 11 | const getFile = async (contentType: string, filename: string): Promise => { 12 | const content = new Uint8Array(await Bun.file(filename).arrayBuffer()); 13 | const header = `HTTP/1.1 200 OK\r\nContent-Type: ${contentType}\r\n\r\n`; 14 | const headerBytes = new TextEncoder().encode(header); 15 | const response = new Uint8Array(headerBytes.length + content.length); 16 | response.set(headerBytes); 17 | response.set(content, headerBytes.length); 18 | return response; 19 | }; 20 | 21 | // Set a custom files handler 22 | async function myFileHandler(myUrl: URL) { 23 | console.log(`File: ${myUrl.pathname}`); 24 | // Index example 25 | if (myUrl.pathname === '/index.html' || myUrl.pathname === '/') { 26 | return await getFile('text/html', 'index.html'); 27 | } 28 | // Custom text string example 29 | if (myUrl.pathname === '/test') { 30 | return "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello"; 31 | } 32 | // File examples 33 | if (myUrl.pathname === '/assets/test_app.js') { 34 | return await getFile('application/javascript', 'assets/test_app.js'); 35 | } 36 | if (myUrl.pathname === '/assets/webui.jpeg') { 37 | return await getFile('image/jpeg', 'assets/webui.jpeg'); 38 | } 39 | // Error 404 example 40 | return "HTTP/1.1 404 Not Found"; 41 | }; 42 | 43 | // Create new window 44 | const myWindow = new WebUI(); 45 | 46 | // Bind Exit 47 | myWindow.bind("exit", () => { 48 | // Close all windows and exit 49 | WebUI.exit(); 50 | }); 51 | 52 | // Set files handler 53 | // Note: Should be called before `.show()` 54 | myWindow.setFileHandler(myFileHandler); 55 | 56 | // Show the window 57 | await myWindow.show('index.html'); 58 | 59 | // Wait until all windows get closed 60 | await WebUI.wait(); 61 | 62 | console.log("Thank you."); 63 | 64 | process.exit(0); 65 | -------------------------------------------------------------------------------- /examples/custom_file_handler/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebUI 2 - Bun File Handler Example 7 | 8 | 9 |

WebUI 2 - Bun File Handler Example

10 |
11 | 12 |
13 | - 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/custom_web_server/custom_web_server.ts: -------------------------------------------------------------------------------- 1 | // To run this script: 2 | // python simple_web_server.py 3 | // bun run custom_web_server.ts 4 | 5 | // To import from local (Debugging and Development) 6 | // import { WebUI } from "../../mod.ts"; 7 | 8 | // To import from NPM (Production) 9 | import { WebUI } from '@webui-dev/bun-webui'; 10 | 11 | async function allEvents(e: WebUI.Event) { 12 | /* 13 | e.window: WebUI; 14 | e.eventType: WebUI.EventType; 15 | e.element: string; 16 | */ 17 | console.log(`\nallEvents: window = '${e.window}'`); 18 | console.log(`allEvents: eventType = '${e.eventType}'`); 19 | console.log(`allEvents: element = '${e.element}'`); 20 | switch (e.eventType) { 21 | case WebUI.EventType.Disconnected: 22 | // Window disconnection event 23 | console.log(`Window closed.`); 24 | break; 25 | case WebUI.EventType.Connected: 26 | // Window connection event 27 | console.log(`Window connected.`); 28 | break; 29 | case WebUI.EventType.MouseClick: 30 | // Mouse click event 31 | console.log(`Mouse click.`); 32 | break; 33 | case WebUI.EventType.Navigation: 34 | // Window navigation event 35 | const url = e.arg.string(0); 36 | console.log(`Navigation to '${url}'`); 37 | // Because we used `webui_bind(MyWindow, "", events);` 38 | // WebUI will block all `href` link clicks and sent here instead. 39 | // We can then control the behaviour of links as needed. 40 | e.window.navigate(url); 41 | break; 42 | case WebUI.EventType.Callback: 43 | // Function call event 44 | console.log(`Function call.`); 45 | break; 46 | } 47 | } 48 | 49 | async function myBackendFunc(e: WebUI.Event) { 50 | const a = e.arg.number(0); // First argument 51 | const b = e.arg.string(1); // Second argument 52 | const c = e.arg.boolean(2); // Third argument 53 | console.log(`\nFirst argument: ${a}`); 54 | console.log(`Second argument: ${b}`); 55 | console.log(`Third argument: ${c}`); 56 | } 57 | 58 | // Create new window 59 | const myWindow = new WebUI(); 60 | 61 | // Bind All Events 62 | myWindow.bind("", allEvents); 63 | 64 | // Bind Backend Function 65 | myWindow.bind("myBackendFunc", myBackendFunc); 66 | 67 | // Bind Exit Function 68 | myWindow.bind("exit", () => { 69 | // Close all windows and exit 70 | WebUI.exit(); 71 | }); 72 | 73 | // Set the web-server/WebSocket port that WebUI should 74 | // use. This means `webui.js` will be available at: 75 | // http://localhost:MY_PORT_NUMBER/webui.js 76 | myWindow.setPort(8081); 77 | 78 | // Show a new window and point to our custom web server 79 | // Assuming the custom web server is running on port 80 | // 8080... 81 | myWindow.show("http://localhost:8080/"); 82 | 83 | // Wait until all windows get closed 84 | await WebUI.wait(); 85 | 86 | console.log("Thank you."); 87 | 88 | process.exit(0); 89 | -------------------------------------------------------------------------------- /examples/custom_web_server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebUI - Custom Web-Server Example (C) 6 | 7 | 8 | 9 | 10 |

Custom Web-Server Example (C)

11 |

12 | This HTML page is handled by a custom Web-Server other than WebUI.
13 | This window is connected to the back-end because we used:

http://localhost:8081/webui.js
14 |

15 |

Simple link example (Local file)

16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/custom_web_server/second.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebUI - Custom Web-Server second page (C) 6 | 7 | 8 | 9 | 10 |

This is the second page !

11 |

Back

12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/custom_web_server/simple_web_server.py: -------------------------------------------------------------------------------- 1 | import http.server 2 | import socketserver 3 | 4 | PORT = 8080 5 | 6 | Handler = http.server.SimpleHTTPRequestHandler 7 | 8 | with socketserver.TCPServer(("", PORT), Handler) as httpd: 9 | print(f"Server started at http://localhost:{PORT}") 10 | httpd.serve_forever() 11 | -------------------------------------------------------------------------------- /examples/hello_world/hello_world.ts: -------------------------------------------------------------------------------- 1 | // To run this script: 2 | // bun run hello_world.ts 3 | 4 | // To import from local (Debugging and Development) 5 | // import { WebUI } from "../../mod.ts"; 6 | 7 | // To import from NPM (Production) 8 | import { WebUI } from '@webui-dev/bun-webui'; 9 | 10 | const myHtml = ` 11 | 12 | 13 | 14 | WebUI 2 - Bun Hello World Example 15 | 41 | 42 | 43 |

WebUI 2 - Bun Hello World


44 | A:

45 | B:

46 |
Result: ?


47 | - - 48 | 66 | 67 | `; 68 | 69 | async function checkResult(e: WebUI.Event) { 70 | const a = e.arg.number(0); 71 | const b = e.arg.number(1); 72 | const res = e.arg.number(2); 73 | if (a + b === res) { 74 | return `Correct: ${a} + ${b} = ${res}`; 75 | } else { 76 | return `Incorrect: ${a} + ${b} != ${res}`; 77 | } 78 | } 79 | 80 | async function calculate(e: WebUI.Event) { 81 | const getA = await e.window.script("return get_A()").catch((error) => { 82 | console.error(`Error in the JavaScript: ${error}`); 83 | return ""; 84 | }); 85 | const getB = await e.window.script("return get_B()").catch((error) => { 86 | console.error(`Error in the JavaScript: ${error}`); 87 | return ""; 88 | }); 89 | const result = parseInt(getA) + parseInt(getB); 90 | e.window.run(`set_result(${result});`); 91 | } 92 | 93 | const myWindow = new WebUI(); 94 | myWindow.bind("calculate", calculate); 95 | myWindow.bind("checkResult", checkResult); 96 | myWindow.bind("exit", () => { 97 | WebUI.exit(); 98 | }); 99 | 100 | myWindow.show(myHtml); 101 | await WebUI.wait(); 102 | 103 | console.log("Thank you."); 104 | 105 | process.exit(0); 106 | -------------------------------------------------------------------------------- /examples/send_raw_binary/send_raw_binary.ts: -------------------------------------------------------------------------------- 1 | // To run this script: 2 | // bun run send_raw_binary.ts 3 | 4 | // To import from local (Debugging and Development) 5 | // import { WebUI } from "../../mod.ts"; 6 | 7 | // To import from NPM (Production) 8 | import { WebUI } from '@webui-dev/bun-webui'; 9 | 10 | const myHtml = ` 11 | 12 | 13 | 14 | WebUI 2 - Bun Send Raw Binary Example 15 | 41 | 42 | 43 |

WebUI 2 - Bun Send Raw Binary Example

44 |
45 |
Received 0 bytes from backend
46 |
47 | Image 48 |
49 | - - 50 | 67 | 68 | `; 69 | 70 | async function get_raw_data(e: WebUI.Event) { 71 | const rawData = new Uint8Array([0x01, 0x02, 0x03]); 72 | console.log(`Sending ${rawData.byteLength} bytes to UI...`); 73 | e.window.sendRaw("processRawData", rawData); 74 | } 75 | 76 | async function get_raw_picture(e: WebUI.Event) { 77 | const pictureRaw = new Uint8Array(await Bun.file("./webui.jpeg").arrayBuffer()); 78 | console.log(`Sending picture file (${pictureRaw.byteLength} bytes) to UI...`); 79 | e.window.sendRaw("setRawImage", pictureRaw); 80 | } 81 | 82 | // Create new window 83 | const myWindow = new WebUI(); 84 | 85 | // Bind 86 | myWindow.bind("get_raw_data", get_raw_data); 87 | myWindow.bind("get_raw_picture", get_raw_picture); 88 | myWindow.bind("exit", () => { 89 | // Close all windows and exit 90 | WebUI.exit(); 91 | }); 92 | 93 | // Show the window 94 | myWindow.show(myHtml); // Or myWindow.show('./myFile.html'); 95 | 96 | // Wait until all windows get closed 97 | await WebUI.wait(); 98 | 99 | console.log("Thank you."); 100 | 101 | process.exit(0); 102 | -------------------------------------------------------------------------------- /examples/send_raw_binary/webui.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webui-dev/bun-webui/bf16e4f9413f16dce2b87dfed11dfe0fec91af73/examples/send_raw_binary/webui.jpeg -------------------------------------------------------------------------------- /img/cppcon_2019.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webui-dev/bun-webui/bf16e4f9413f16dce2b87dfed11dfe0fec91af73/img/cppcon_2019.png -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webui-dev/bun-webui/bf16e4f9413f16dce2b87dfed11dfe0fec91af73/img/screenshot.png -------------------------------------------------------------------------------- /img/webui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webui-dev/bun-webui/bf16e4f9413f16dce2b87dfed11dfe0fec91af73/img/webui.png -------------------------------------------------------------------------------- /img/webui_bun_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webui-dev/bun-webui/bf16e4f9413f16dce2b87dfed11dfe0fec91af73/img/webui_bun_example.png -------------------------------------------------------------------------------- /img/webui_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webui-dev/bun-webui/bf16e4f9413f16dce2b87dfed11dfe0fec91af73/img/webui_diagram.png -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * # Bun WebUI 3 | * 4 | * > Use any web browser as GUI, with Bun in the backend and HTML5 in the 5 | * > frontend, all in a lightweight Bun module. 6 | * 7 | * ## Features 8 | * 9 | * - Fully Independent (_No need for any third-party runtimes_) 10 | * - Lightweight _~900 Kb_ for the whole package & Small memory footprint 11 | * - Fast binary communication protocol between WebUI and the browser (_Instead of JSON_) 12 | * - Multi-platform & Multi-Browser 13 | * - Using private profile for safety 14 | * - Original library written in Pure C 15 | * 16 | * ## Minimal Example 17 | * 18 | * ```ts 19 | * import { WebUI } from '@webui-dev/bun-webui'; 20 | * 21 | * const myWindow = new WebUI(); 22 | * myWindow.show(" Hello World! "); 23 | * await WebUI.wait(); 24 | * ``` 25 | * 26 | * @module 27 | * @license MIT 28 | */ 29 | export { WebUI } from "./src/webui.ts"; 30 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webui-dev/bun-webui", 3 | "version": "2.5.2", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@webui-dev/bun-webui", 9 | "version": "2.5.2", 10 | "license": "MIT" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webui-dev/bun-webui", 3 | "version": "2.5.2", 4 | "description": "Use any web browser as GUI, with Bun in the backend and HTML5 in the frontend.", 5 | "main": "mod.ts", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 0" 11 | }, 12 | "keywords": ["gui", "ui", "webui", "browser", "webview"], 13 | "author": "Hassan Draga", 14 | "license": "MIT" 15 | } 16 | -------------------------------------------------------------------------------- /src/bootstrap.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | SETLOCAL 3 | 4 | :: This script downloads the trusted WebUI compiled library by GitHub CI for Windows. 5 | 6 | IF "%1"=="minimal" ( 7 | goto MINIMAL 8 | ) 9 | 10 | :: --- Full ------------------------------------- 11 | :: Download WebUI library for all supported OS. 12 | echo WebUI Bun Bootstrap 13 | echo. 14 | 15 | :: Creating the temporary cache folder 16 | mkdir "cache" 2>nul 1>nul 17 | 18 | :: Nightly Build 19 | :: SET "LINUX_ARM=https://github.com/webui-dev/webui/releases/download/nightly/webui-linux-gcc-arm.zip" 20 | :: SET "LINUX_ARM64=https://github.com/webui-dev/webui/releases/download/nightly/webui-linux-gcc-arm64.zip" 21 | :: SET "LINUX_X64=https://github.com/webui-dev/webui/releases/download/nightly/webui-linux-gcc-x64.zip" 22 | :: SET "MACOS_ARM64=https://github.com/webui-dev/webui/releases/download/nightly/webui-macos-clang-arm64.zip" 23 | :: SET "MACOS_X64=https://github.com/webui-dev/webui/releases/download/nightly/webui-macos-clang-x64.zip" 24 | :: SET "WINDOWS_MSVC_X64=https://github.com/webui-dev/webui/releases/download/nightly/webui-windows-msvc-x64.zip" 25 | 26 | :: Release 27 | SET "LINUX_ARM=https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/webui-linux-gcc-arm.zip" 28 | SET "LINUX_ARM64=https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/webui-linux-gcc-arm64.zip" 29 | SET "LINUX_X64=https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/webui-linux-gcc-x64.zip" 30 | SET "MACOS_ARM64=https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/webui-macos-clang-arm64.zip" 31 | SET "MACOS_X64=https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/webui-macos-clang-x64.zip" 32 | SET "WINDOWS_MSVC_X64=https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/webui-windows-msvc-x64.zip" 33 | 34 | :: Download and extract archives 35 | CALL :DOWNLOAD_AND_EXTRACT %LINUX_ARM% webui-linux-gcc-arm libwebui-2.so 36 | CALL :DOWNLOAD_AND_EXTRACT %LINUX_ARM64% webui-linux-gcc-arm64 libwebui-2.so 37 | CALL :DOWNLOAD_AND_EXTRACT %LINUX_X64% webui-linux-gcc-x64 libwebui-2.so 38 | CALL :DOWNLOAD_AND_EXTRACT %MACOS_ARM64% webui-macos-clang-arm64 libwebui-2.dylib 39 | CALL :DOWNLOAD_AND_EXTRACT %MACOS_X64% webui-macos-clang-x64 libwebui-2.dylib 40 | CALL :DOWNLOAD_AND_EXTRACT %WINDOWS_MSVC_X64% webui-windows-msvc-x64 webui-2.dll 41 | 42 | :: Remove cache folder 43 | echo * Cleaning... 44 | rmdir /S /Q "cache" 2>nul 1>nul 45 | exit /b 46 | 47 | :: Download and Extract Function 48 | :DOWNLOAD_AND_EXTRACT 49 | echo * Downloading [%1]... 50 | SET FULL_URL=%1 51 | SET FILE_NAME=%2 52 | SET LIB_DYN=%3 53 | SET LIB_STATIC=%4 54 | powershell -Command "Invoke-WebRequest '%FULL_URL%' -OutFile 'cache\%FILE_NAME%.zip'" 55 | echo * Extracting [%FILE_NAME%.zip]... 56 | mkdir "cache\%FILE_NAME%" 2>nul 1>nul 57 | tar -xf "cache\%FILE_NAME%.zip" -C "cache" 58 | IF NOT "%LIB_DYN%"=="" ( 59 | :: Copy dynamic library 60 | echo * Copying [%LIB_DYN%]... 61 | mkdir "%FILE_NAME%" 2>nul 1>nul 62 | copy /Y "cache\%FILE_NAME%\%LIB_DYN%" "%FILE_NAME%\%LIB_DYN%" 2>nul 1>nul 63 | ) 64 | IF NOT "%LIB_STATIC%"=="" ( 65 | :: Copy dynamic library 66 | echo * Copying [%LIB_STATIC%]... 67 | mkdir "%FILE_NAME%" 2>nul 1>nul 68 | copy /Y "cache\%FILE_NAME%\%LIB_STATIC%" "%FILE_NAME%\%LIB_STATIC%" 2>nul 1>nul 69 | ) 70 | GOTO :EOF 71 | 72 | :: --- Minimal ---------------------------------- 73 | :: Download WebUI library for only the current OS. 74 | :MINIMAL 75 | 76 | SET "BASE_URL=https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/" 77 | 78 | :: Check the CPU architecture 79 | IF "%PROCESSOR_ARCHITECTURE%"=="x86" ( 80 | :: x86 32Bit 81 | :: SET "FILENAME=webui-windows-msvc-x86" 82 | ECHO Error: Windows x86 32Bit architecture is not supported yet 83 | exit /b 84 | ) ELSE IF "%PROCESSOR_ARCHITECTURE%"=="AMD64" ( 85 | :: x86 64Bit 86 | SET "FILENAME=webui-windows-msvc-x64" 87 | ) ELSE IF "%PROCESSOR_ARCHITECTURE%"=="ARM" ( 88 | :: ARM 32Bit 89 | :: SET "FILENAME=webui-windows-msvc-arm" 90 | ECHO Error: Windows ARM architecture is unsupported yet 91 | exit /b 92 | ) ELSE IF "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( 93 | :: ARM 64Bit 94 | :: SET "FILENAME=webui-windows-msvc-arm64" 95 | ECHO Error: Windows ARM64 architecture is unsupported yet 96 | exit /b 97 | ) ELSE ( 98 | ECHO Error: Unknown architecture '%PROCESSOR_ARCHITECTURE%' 99 | exit /b 100 | ) 101 | 102 | :: Creating the temporary cache folder 103 | mkdir "cache" 2>nul 1>nul 104 | mkdir "cache\%FILENAME%" 2>nul 1>nul 105 | 106 | :: Download the archive using PowerShell 107 | powershell -Command "Invoke-WebRequest '%BASE_URL%%FILENAME%.zip' -OutFile 'cache\%FILENAME%.zip'" 108 | 109 | :: Extract archive (Windows 10 and later) 110 | tar -xf "cache\%FILENAME%.zip" -C "cache" 111 | 112 | :: Copy library 113 | mkdir "%FILENAME%" 2>nul 1>nul 114 | copy /Y "cache\%FILENAME%\webui-2.dll" "%FILENAME%\webui-2.dll" 2>nul 1>nul 115 | 116 | :: Remove cache folder 117 | rmdir /S /Q "cache" 2>nul 1>nul 118 | 119 | ENDLOCAL 120 | -------------------------------------------------------------------------------- /src/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script downloads the trusted WebUI compiled library by GitHub CI for Linux. 4 | 5 | if [[ "$1" == "" ]]; then 6 | 7 | # --- Full ------------------------------------- 8 | # Download WebUI library for all supported OS. 9 | echo "WebUI Bun Bootstrap" 10 | echo 11 | 12 | # Creating the temporary cache folder 13 | mkdir -p "cache" 2>/dev/null 14 | 15 | # Nightly Build 16 | # LINUX_ARM="https://github.com/webui-dev/webui/releases/download/nightly/webui-linux-gcc-arm.zip" 17 | # LINUX_ARM64="https://github.com/webui-dev/webui/releases/download/nightly/webui-linux-gcc-arm64.zip" 18 | # LINUX_X64="https://github.com/webui-dev/webui/releases/download/nightly/webui-linux-gcc-x64.zip" 19 | # MACOS_ARM64="https://github.com/webui-dev/webui/releases/download/nightly/webui-macos-clang-arm64.zip" 20 | # MACOS_X64="https://github.com/webui-dev/webui/releases/download/nightly/webui-macos-clang-x64.zip" 21 | # WINDOWS_MSVC_X64="https://github.com/webui-dev/webui/releases/download/nightly/webui-windows-msvc-x64.zip" 22 | 23 | # Release 24 | LINUX_ARM="https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/webui-linux-gcc-arm.zip" 25 | LINUX_ARM64="https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/webui-linux-gcc-arm64.zip" 26 | LINUX_X64="https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/webui-linux-gcc-x64.zip" 27 | MACOS_ARM64="https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/webui-macos-clang-arm64.zip" 28 | MACOS_X64="https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/webui-macos-clang-x64.zip" 29 | WINDOWS_MSVC_X64="https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/webui-windows-msvc-x64.zip" 30 | 31 | # Download and extract archives 32 | download_and_extract() { 33 | echo "* Downloading [$1]..." 34 | wget -q "$1" -O "cache/$2.zip" 35 | echo "* Extracting [$2.zip]..." 36 | mkdir -p "cache/$2" 2>/dev/null 37 | unzip -q "cache/$2.zip" -d "cache" 38 | if [ -n "$3" ]; then 39 | echo "* Copying [$3]..." 40 | mkdir -p "$2" 2>/dev/null 41 | cp -f "cache/$2/$3" "$2/$3" 42 | fi 43 | if [ -n "$4" ]; then 44 | echo "* Copying [$4]..." 45 | mkdir -p "$2" 2>/dev/null 46 | cp -f "cache/$2/$4" "$2/$4" 47 | fi 48 | } 49 | 50 | download_and_extract $LINUX_ARM "webui-linux-gcc-arm" "libwebui-2.so" 51 | download_and_extract $LINUX_ARM64 "webui-linux-gcc-arm64" "libwebui-2.so" 52 | download_and_extract $LINUX_X64 "webui-linux-gcc-x64" "libwebui-2.so" 53 | download_and_extract $MACOS_ARM64 "webui-macos-clang-arm64" "libwebui-2.dylib" 54 | download_and_extract $MACOS_X64 "webui-macos-clang-x64" "libwebui-2.dylib" 55 | download_and_extract $WINDOWS_MSVC_X64 "webui-windows-msvc-x64" "webui-2.dll" 56 | 57 | # Remove cache folder 58 | echo "* Cleaning..." 59 | rm -rf "cache" 60 | exit 0 61 | fi 62 | 63 | if [[ "$1" == "minimal" ]]; then 64 | 65 | # --- Minimal ---------------------------------- 66 | # Download WebUI library for only the current OS. 67 | 68 | # Nightly Build 69 | # BASE_URL="https://github.com/webui-dev/webui/releases/download/nightly/" 70 | 71 | # Release 72 | BASE_URL="https://github.com/webui-dev/webui/releases/download/2.5.0-beta.3/" 73 | 74 | # Detect OS (macOS / Linux) 75 | OS="linux" 76 | CC="gcc" 77 | EXT="so" 78 | if [[ "$OSTYPE" == "darwin"* ]]; then 79 | OS="macos" 80 | CC="clang" 81 | EXT="dylib" 82 | fi 83 | 84 | # Check the CPU architecture 85 | ARCH=$(uname -m) 86 | if [ "$ARCH" = "x86" ]; then 87 | # x86 32Bit 88 | # FILENAME="webui-${OS}-${CC}-x86" 89 | echo "Error: Linux/macOS x86 32Bit architecture is not supported yet" 90 | exit 1 91 | elif [ "$ARCH" = "x86_64" ]; then 92 | # x86 64Bit 93 | FILENAME="webui-${OS}-${CC}-x64" 94 | elif [ "$ARCH" = "arm" ]; then 95 | # ARM 32Bit 96 | FILENAME="webui-${OS}-${CC}-arm" 97 | elif [ "$ARCH" = "aarch64" ]; then 98 | # ARM 64Bit 99 | FILENAME="webui-${OS}-${CC}-arm64" 100 | else 101 | echo "Error: Unknown architecture '$ARCH'" 102 | exit 1 103 | fi 104 | 105 | # Creating the temporary cache folder 106 | mkdir -p "cache/$FILENAME" 2>/dev/null 107 | 108 | # Download the archive using wget 109 | wget -q "$BASE_URL$FILENAME.zip" -O "cache/$FILENAME.zip" 110 | 111 | # Extract archive 112 | unzip -q "cache/$FILENAME.zip" -d "cache" 113 | 114 | # Copy library 115 | mkdir -p "$FILENAME" 2>/dev/null 116 | cp -f "cache/$FILENAME/webui-2.${EXT}" "$FILENAME/webui-2.${EXT}" 117 | 118 | # Remove cache folder 119 | rm -rf "cache" 120 | 121 | exit 0 122 | fi 123 | -------------------------------------------------------------------------------- /src/ffi_worker.ts: -------------------------------------------------------------------------------- 1 | /* 2 | WebUI Bun 2.5.2 3 | http://webui.me 4 | https://github.com/webui-dev/bun-webui 5 | Copyright (c) 2020-2025 Hassan Draga. 6 | Licensed under MIT License. 7 | All rights reserved. 8 | Canada. 9 | */ 10 | 11 | import { JSCallback } from "bun:ffi"; 12 | import { loadLib } from "./lib.ts"; 13 | import { toCString } from "./utils.ts"; 14 | 15 | // Load the WebUI library inside the worker 16 | const _lib = loadLib(); 17 | 18 | self.onmessage = (event: MessageEvent) => { 19 | const { id, action, data } = event.data; 20 | if (action === "bind") { 21 | // Bind 22 | // We are safe here to bind the callback with WebUI 23 | const windowId = BigInt(data.windowId); 24 | const elementId = data.elementId; 25 | const callbackIndex = data.callbackIndex; 26 | const callbackResource = new JSCallback( 27 | ( 28 | _param_window: number | bigint, 29 | _param_event_type: number | bigint, 30 | _param_element: number, 31 | _param_event_number: number | bigint, 32 | _param_bind_id: number | bigint 33 | ) => { 34 | self.postMessage({ 35 | action: "invokeCallback", 36 | data: { 37 | callbackIndex, 38 | param_window: _param_window, 39 | param_event_type: _param_event_type, 40 | param_element: _param_element, 41 | param_event_number: _param_event_number, 42 | param_bind_id: _param_bind_id, 43 | }, 44 | }); 45 | }, 46 | { 47 | returns: "void", 48 | args: ["usize", "usize", "pointer", "usize", "usize"], 49 | threadsafe: true, 50 | } 51 | ); 52 | _lib.symbols.webui_interface_bind(windowId, toCString(elementId), callbackResource); 53 | self.postMessage({ id, result: "bind_success" }); 54 | } else if (action === "setFileHandler") { 55 | // File Handler 56 | // We are safe here to bind the file handler callback with WebUI 57 | const windowId = BigInt(data.windowId); 58 | const callbackIndex = data.callbackIndex; 59 | const callbackResource = new JSCallback( 60 | ( 61 | _param_url: number, 62 | _param_length: number 63 | ) => { 64 | self.postMessage({ 65 | action: "invokeFileHandler", 66 | data: { 67 | callbackIndex, 68 | windowId, 69 | param_url: _param_url, 70 | param_length: _param_length, 71 | }, 72 | }); 73 | }, 74 | { 75 | returns: "pointer", 76 | args: ["pointer", "pointer"], 77 | threadsafe: true, 78 | } 79 | ); 80 | _lib.symbols.webui_set_file_handler(windowId, callbackResource); 81 | self.postMessage({ id, result: "setFileHandler_success" }); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | // Bun WebUI 2 | // FFI (Foreign Function Interface) for webui.ts 3 | 4 | import { dlopen, suffix } from "bun:ffi"; 5 | import { libName } from "../deps.ts"; 6 | 7 | export function loadLib() { 8 | return dlopen( 9 | libName, 10 | { 11 | webui_wait: { 12 | // void webui_wait(void) 13 | args: [], 14 | returns: "void", 15 | nonblocking: true, 16 | }, 17 | webui_new_window: { 18 | // size_t webui_new_window(void) 19 | args: [], 20 | returns: "usize", 21 | }, 22 | webui_show: { 23 | // bool webui_show(size_t window, const char* content) 24 | args: ["usize", "buffer"], 25 | returns: "bool", 26 | }, 27 | webui_show_browser: { 28 | // bool webui_show_browser(size_t window, const char* content, size_t browser) 29 | args: ["usize", "buffer", "usize"], 30 | returns: "bool", 31 | }, 32 | webui_interface_bind: { 33 | // size_t webui_interface_bind(size_t window, const char* element, void (*func)(size_t, size_t, char*, size_t, size_t)) 34 | args: ["usize", "buffer", "function"], 35 | returns: "usize", 36 | }, 37 | webui_script: { 38 | // bool webui_script(size_t window, const char* script, size_t timeout, char* buffer, size_t buffer_length) 39 | args: ["usize", "buffer", "usize", "buffer", "usize"], 40 | returns: "bool", 41 | }, 42 | webui_run: { 43 | // void webui_run(size_t window, const char* script) 44 | args: ["usize", "buffer"], 45 | returns: "void", 46 | }, 47 | webui_interface_set_response: { 48 | // void webui_interface_set_response(size_t window, size_t event_number, const char* response) 49 | args: ["usize", "usize", "buffer"], 50 | returns: "void", 51 | }, 52 | webui_exit: { 53 | // void webui_exit(void) 54 | args: [], 55 | returns: "void", 56 | }, 57 | webui_is_shown: { 58 | // bool webui_is_shown(size_t window) 59 | args: ["usize"], 60 | returns: "bool", 61 | }, 62 | webui_close: { 63 | // void webui_close(size_t window) 64 | args: ["usize"], 65 | returns: "void", 66 | }, 67 | webui_set_file_handler: { 68 | // void webui_set_file_handler(size_t window, const void* (*handler)(const char* filename, int* length)) 69 | args: ["usize", "function"], 70 | returns: "void", 71 | }, 72 | webui_interface_is_app_running: { 73 | // bool webui_interface_is_app_running(void) 74 | args: [], 75 | returns: "bool", 76 | }, 77 | webui_set_profile: { 78 | // void webui_set_profile(size_t window, const char* name, const char* path) 79 | args: ["usize", "buffer", "buffer"], 80 | returns: "void", 81 | }, 82 | webui_interface_get_int_at: { 83 | // long long int webui_interface_get_int_at(size_t window, size_t event_number, size_t index) 84 | args: ["usize", "usize", "usize"], 85 | returns: "i64", 86 | }, 87 | webui_interface_get_string_at: { 88 | // const char* webui_interface_get_string_at(size_t window, size_t event_number, size_t index) 89 | // Change return type from "buffer" to "pointer" 90 | args: ["usize", "usize", "usize"], 91 | returns: "pointer", 92 | }, 93 | webui_interface_get_bool_at: { 94 | // bool webui_interface_get_bool_at(size_t window, size_t event_number, size_t index) 95 | args: ["usize", "usize", "usize"], 96 | returns: "bool", 97 | }, 98 | webui_clean: { 99 | // void webui_clean() 100 | args: [], 101 | returns: "void", 102 | }, 103 | webui_set_root_folder: { 104 | // bool webui_set_root_folder(size_t window, const char* path) 105 | args: ["usize", "buffer"], 106 | returns: "bool", 107 | }, 108 | webui_set_tls_certificate: { 109 | // bool webui_set_tls_certificate(const char* certificate_pem, const char* private_key_pem) 110 | args: ["buffer", "buffer"], 111 | returns: "bool", 112 | }, 113 | webui_set_kiosk: { 114 | // void webui_set_kiosk(size_t window, bool status) 115 | args: ["usize", "bool"], 116 | returns: "void", 117 | }, 118 | webui_destroy: { 119 | // void webui_destroy(size_t window) 120 | args: ["usize"], 121 | returns: "void", 122 | }, 123 | webui_set_timeout: { 124 | // void webui_set_timeout(size_t second) 125 | args: ["usize"], 126 | returns: "void", 127 | }, 128 | webui_set_icon: { 129 | // void webui_set_icon(size_t window, const char* icon, const char* icon_type) 130 | args: ["usize", "buffer", "buffer"], 131 | returns: "void", 132 | }, 133 | webui_encode: { 134 | // char* webui_encode(const char* str) 135 | // Change return type from "buffer" to "pointer" 136 | args: ["buffer"], 137 | returns: "pointer", 138 | }, 139 | webui_decode: { 140 | // char* webui_decode(const char* str) 141 | // Change return type from "buffer" to "pointer" 142 | args: ["buffer"], 143 | returns: "pointer", 144 | }, 145 | webui_free: { 146 | // void webui_free(void* ptr) 147 | args: ["pointer"], 148 | returns: "void", 149 | }, 150 | webui_malloc: { 151 | // void* webui_malloc(size_t size) 152 | args: ["usize"], 153 | returns: "pointer", 154 | }, 155 | webui_send_raw: { 156 | // void webui_send_raw(size_t window, const char* function, const void* raw, size_t size) 157 | args: ["usize", "buffer", "buffer", "usize"], 158 | returns: "void", 159 | }, 160 | webui_set_hide: { 161 | // void webui_set_hide(size_t window, bool status) 162 | args: ["usize", "bool"], 163 | returns: "void", 164 | }, 165 | webui_set_size: { 166 | // void webui_set_size(size_t window, unsigned int width, unsigned int height) 167 | args: ["usize", "u32", "u32"], 168 | returns: "void", 169 | }, 170 | webui_set_position: { 171 | // void webui_set_position(size_t window, unsigned int x, unsigned int y) 172 | args: ["usize", "u32", "u32"], 173 | returns: "void", 174 | }, 175 | webui_get_url: { 176 | // const char* webui_get_url(size_t window) 177 | // Change return type from "buffer" to "pointer" 178 | args: ["usize"], 179 | returns: "pointer", 180 | }, 181 | webui_set_public: { 182 | // void webui_set_public(size_t window, bool status) 183 | args: ["usize", "bool"], 184 | returns: "void", 185 | }, 186 | webui_navigate: { 187 | // void webui_navigate(size_t window, const char* url) 188 | args: ["usize", "buffer"], 189 | returns: "void", 190 | }, 191 | webui_delete_all_profiles: { 192 | // void webui_delete_all_profiles(void) 193 | args: [], 194 | returns: "void", 195 | }, 196 | webui_delete_profile: { 197 | // void webui_delete_profile(size_t window) 198 | args: ["usize"], 199 | returns: "void", 200 | }, 201 | webui_get_parent_process_id: { 202 | // size_t webui_get_parent_process_id(size_t window) 203 | args: ["usize"], 204 | returns: "usize", 205 | }, 206 | webui_get_child_process_id: { 207 | // size_t webui_get_child_process_id(size_t window) 208 | args: ["usize"], 209 | returns: "usize", 210 | }, 211 | webui_set_port: { 212 | // bool webui_set_port(size_t window, size_t port) 213 | args: ["usize", "usize"], 214 | returns: "bool", 215 | }, 216 | webui_set_runtime: { 217 | // void webui_set_runtime(size_t window, size_t runtime) 218 | args: ["usize", "usize"], 219 | returns: "void", 220 | }, 221 | webui_set_config: { 222 | // void webui_set_config(webui_config option, bool status) 223 | // show_wait_connection: 0 224 | // ui_event_blocking: 1 225 | // folder_monitor: 2 226 | // multi_client: 3 227 | // use_cookies: 4 228 | // asynchronous_response: 5 229 | args: ["usize", "bool"], 230 | returns: "void", 231 | }, 232 | webui_interface_show_client: { 233 | // bool webui_interface_show_client(size_t window, size_t event_number, const char* content) 234 | args: ["usize", "usize", "buffer"], 235 | returns: "bool", 236 | }, 237 | webui_interface_close_client: { 238 | // void webui_interface_close_client(size_t window, size_t event_number) 239 | args: ["usize", "usize"], 240 | returns: "void", 241 | }, 242 | webui_interface_send_raw_client: { 243 | // void webui_interface_send_raw_client( 244 | // size_t window, size_t event_number, const char* function, const void* raw, size_t size) 245 | args: ["usize", "usize", "buffer", "buffer", "usize"], 246 | returns: "void", 247 | }, 248 | webui_interface_navigate_client: { 249 | // void webui_interface_navigate_client(size_t window, size_t event_number, const char* url) 250 | args: ["usize", "usize", "buffer"], 251 | returns: "void", 252 | }, 253 | webui_interface_run_client: { 254 | // void webui_interface_run_client(size_t window, size_t event_number, const char* script) 255 | args: ["usize", "usize", "buffer"], 256 | returns: "void", 257 | }, 258 | webui_interface_script_client: { 259 | // bool webui_interface_script_client( 260 | // size_t window, size_t event_number, const char* script, size_t timeout, char* buffer, size_t buffer_length) 261 | args: ["usize", "usize", "buffer", "usize", "buffer", "usize"], 262 | returns: "bool", 263 | }, 264 | webui_send_raw_client: { 265 | // void webui_send_raw_client(webui_event_t* e, const char* function, const void* raw, size_t size) 266 | args: ["pointer", "buffer", "buffer", "usize"], 267 | returns: "void", 268 | }, 269 | webui_interface_set_response_file_handler: { 270 | // void webui_interface_set_response_file_handler(size_t window, const void* response, int length) 271 | args: ["usize", "pointer", "usize"], 272 | returns: "void", 273 | }, 274 | webui_get_best_browser: { 275 | // size_t webui_get_best_browser(size_t window) 276 | args: ["usize"], 277 | returns: "usize", 278 | }, 279 | webui_start_server: { 280 | // const char* webui_start_server(size_t window, const char* content) 281 | // Change return type from "buffer" to "pointer" 282 | args: ["usize", "buffer"], 283 | returns: "pointer", 284 | }, 285 | webui_show_wv: { 286 | // bool webui_show_wv(size_t window, const char* content) 287 | args: ["usize", "buffer"], 288 | returns: "bool", 289 | }, 290 | webui_set_custom_parameters: { 291 | // void webui_set_custom_parameters(size_t window, char *params) 292 | args: ["usize", "buffer"], 293 | returns: "void", 294 | }, 295 | webui_set_high_contrast: { 296 | // void webui_set_high_contrast(size_t window, bool status) 297 | args: ["usize", "bool"], 298 | returns: "void", 299 | }, 300 | webui_is_high_contrast: { 301 | // bool webui_is_high_contrast(void) 302 | args: [], 303 | returns: "bool", 304 | }, 305 | webui_browser_exist: { 306 | // bool webui_browser_exist(size_t browser) 307 | args: ["usize"], 308 | returns: "bool", 309 | }, 310 | webui_set_default_root_folder: { 311 | // bool webui_set_default_root_folder(const char* path) 312 | args: ["buffer"], 313 | returns: "bool", 314 | }, 315 | webui_set_minimum_size: { 316 | // void webui_set_minimum_size(size_t window, unsigned int width, unsigned int height) 317 | args: ["usize", "u32", "u32"], 318 | returns: "void", 319 | }, 320 | webui_set_proxy: { 321 | // void webui_set_proxy(size_t window, const char* proxy_server) 322 | args: ["usize", "buffer"], 323 | returns: "void", 324 | }, 325 | webui_open_url: { 326 | // void webui_open_url(const char* url) 327 | args: ["buffer"], 328 | returns: "void", 329 | }, 330 | webui_get_free_port: { 331 | // size_t webui_get_free_port(void) 332 | args: [], 333 | returns: "usize", 334 | }, 335 | webui_memcpy: { 336 | // void webui_memcpy(void* dest, void* src, size_t count) 337 | args: ["pointer", "pointer", "usize"], 338 | returns: "void", 339 | }, 340 | } as const, 341 | ); 342 | } 343 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { WebUI } from "../mod.ts"; 2 | import { loadLib } from "./lib.ts"; 3 | 4 | export type Usize = number | bigint; 5 | 6 | export type BindCallback = (event: WebUIEvent) => T | Promise; 7 | export type BindFileHandlerCallback = (url: URL) => T | Promise; 8 | 9 | export interface WebUIEvent { 10 | window: WebUI; 11 | eventType: number; 12 | eventNumber: number; 13 | element: string; 14 | arg: { 15 | number: (index: number) => number; 16 | string: (index: number) => string; 17 | boolean: (index: number) => boolean; 18 | }; 19 | } 20 | 21 | export type WebUILib = Awaited>; 22 | 23 | export type Datatypes = string | number | boolean; 24 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | // Bun WebUI 2 | // Utilities 3 | 4 | import { promises as fs } from "fs"; 5 | import { spawn } from "bun"; 6 | 7 | // The WebUI core version to download 8 | const WebUICoreVersion = "2.5.0-beta.3"; 9 | 10 | /** 11 | * Combine paths using the OS-specific separator. 12 | * Replaces multiple separators with a single one. 13 | */ 14 | function joinPath(...segments: string[]): string { 15 | const isWindows = process.platform === "win32"; 16 | const separator = isWindows ? "\\" : "/"; 17 | const joinedPath = segments.join(separator).replace(/[\/\\]+/g, separator); 18 | return joinedPath; 19 | } 20 | 21 | /** 22 | * Download a file from the Internet and save it to the destination. 23 | */ 24 | async function downloadFile(url: string, dest: string) { 25 | const res = await fetch(url); 26 | const fileData = new Uint8Array(await res.arrayBuffer()); 27 | await fs.writeFile(dest, fileData); 28 | } 29 | 30 | /** 31 | * Run a system command. 32 | */ 33 | async function runCommand(cmd: string, args: string[]): Promise { 34 | const process = Bun.spawn({ 35 | cmd: [cmd, ...args], 36 | stdout: "pipe", 37 | stderr: "pipe", 38 | }); 39 | const exitCode = await process.exited; 40 | if (exitCode !== 0) { 41 | throw new Error(`Command "${cmd}" failed with exit code ${exitCode}`); 42 | } 43 | } 44 | 45 | /** 46 | * Create a directory. Uses recursive mkdir. 47 | */ 48 | async function createDirectory(dirPath: string): Promise { 49 | await fs.mkdir(dirPath, { recursive: true }); 50 | } 51 | 52 | /** 53 | * Copy a file from srcPath to destPath, overwriting if necessary. 54 | */ 55 | async function copyFileOverwrite(srcPath: string, destPath: string) { 56 | try { 57 | await fs.rm(destPath); 58 | } catch (error) { 59 | if ((error as any).code !== "ENOENT") { 60 | throw error; 61 | } 62 | } 63 | await fs.copyFile(srcPath, destPath); 64 | } 65 | 66 | /** 67 | * Get current module full folder path. 68 | */ 69 | export const currentModulePath = (() => { 70 | let directory = new URL(import.meta.url).pathname; 71 | const isWindows = process.platform === "win32"; 72 | if (isWindows) { 73 | if (directory.startsWith("/")) { 74 | // Remove first '/' 75 | directory = directory.slice(1); 76 | } 77 | // Replace all forward slashes with backslashes for Windows paths 78 | directory = directory.replaceAll("/", "\\"); 79 | } 80 | const pathSeparator = isWindows ? "\\" : "/"; 81 | const lastIndex = directory.lastIndexOf(pathSeparator); 82 | directory = directory.substring(0, lastIndex + 1); 83 | if (directory === "") { 84 | return "." + pathSeparator; 85 | } 86 | if (directory.startsWith("/x/")) { 87 | return "." + pathSeparator + directory.slice(1).replace(/\//g, pathSeparator); 88 | } 89 | return directory; 90 | })(); 91 | 92 | /** 93 | * Check if a file exists. 94 | */ 95 | export async function fileExists(filePath: string): Promise { 96 | try { 97 | await fs.stat(filePath); 98 | return true; 99 | } catch (error) { 100 | if ((error as any).code === "ENOENT") { 101 | return false; 102 | } 103 | throw error; 104 | } 105 | } 106 | 107 | /** 108 | * Download the core WebUI library. 109 | */ 110 | export async function downloadCoreLibrary() { 111 | // Base URL 112 | // const baseUrl = `https://github.com/webui-dev/webui/releases/download/${WebUICoreVersion}/`; 113 | const baseUrl = `https://github.com/webui-dev/webui/releases/download/nightly/`; 114 | // Detect OS 115 | let os: string, cc: string, ext: string; 116 | if (process.platform === "darwin") { 117 | os = "macos"; 118 | cc = "clang"; 119 | ext = "dylib"; 120 | } else if (process.platform === "win32") { 121 | os = "windows"; 122 | cc = "msvc"; 123 | ext = "dll"; 124 | } else { 125 | os = "linux"; 126 | cc = "gcc"; 127 | ext = "so"; 128 | } 129 | // Detect Architecture 130 | const archMap: Record = { 131 | "x86": "x86", 132 | "x64": "x64", 133 | "arm": "arm", 134 | "arm64": "arm64", 135 | }; 136 | const arch = archMap[process.arch]; 137 | if (!arch) { 138 | console.error(`Error: Unsupported architecture '${process.arch}'`); 139 | return; 140 | } 141 | 142 | // Construct file name and download URL 143 | const cacheDir = joinPath(currentModulePath, "cache"); 144 | const fileName = `webui-${os}-${cc}-${arch}`; 145 | const fileUrl = `${baseUrl}${fileName}.zip`; 146 | const outputDir = joinPath(currentModulePath, fileName); 147 | 148 | // Create cache directory 149 | await createDirectory(cacheDir); 150 | 151 | // Download the archive 152 | const zipPath = joinPath(cacheDir, `${fileName}.zip`); 153 | await downloadFile(fileUrl, zipPath); 154 | 155 | // Extract the archive 156 | if (process.platform === "win32") { 157 | await runCommand("tar", ["-xf", zipPath, "-C", cacheDir]); 158 | } else { 159 | await runCommand("unzip", ["-o", "-q", zipPath, "-d", cacheDir]); 160 | } 161 | 162 | // Copy library 163 | const libFile = process.platform === "win32" ? `webui-2.${ext}` : `libwebui-2.${ext}`; 164 | await createDirectory(outputDir); 165 | await copyFileOverwrite(joinPath(cacheDir, fileName, libFile), joinPath(outputDir, libFile)); 166 | 167 | // Remove cache directory 168 | await fs.rm(cacheDir, { recursive: true, force: true }); 169 | } 170 | 171 | /** 172 | * Convert a string to a C-style string (null-terminated). 173 | */ 174 | export function toCString(value: string): Uint8Array { 175 | return new TextEncoder().encode(value + "\0"); 176 | } 177 | 178 | /** 179 | * Convert a C-style string (Uint8Array) to a JavaScript string. 180 | */ 181 | export function fromCString(value: Uint8Array): string { 182 | const end = value.findIndex((byte) => byte === 0x00); 183 | return new TextDecoder().decode(value.slice(0, end)); 184 | } 185 | 186 | export class WebUIError extends Error {} 187 | -------------------------------------------------------------------------------- /src/webui.ts: -------------------------------------------------------------------------------- 1 | /* 2 | WebUI Bun 2.5.2 3 | http://webui.me 4 | https://github.com/webui-dev/bun-webui 5 | Copyright (c) 2020-2025 Hassan Draga. 6 | Licensed under MIT License. 7 | All rights reserved. 8 | Canada. 9 | */ 10 | 11 | import { CString } from "bun:ffi"; 12 | import { loadLib } from "./lib.ts"; 13 | import { 14 | BindCallback, 15 | BindFileHandlerCallback, 16 | Datatypes, 17 | Usize, 18 | WebUIEvent, 19 | WebUILib, 20 | } from "./types.ts"; 21 | import { fromCString, toCString, WebUIError } from "./utils.ts"; 22 | 23 | // Register windows to bind instance to WebUI.Event 24 | const windows: Map = new Map(); 25 | 26 | // Global lib entry 27 | let _lib: WebUILib; 28 | 29 | // Bun Worker 30 | // Since Bun is not fully thread-safe, making WebUI calling `.bind()` cause Bun to crash, so instead 31 | // we should use workers with option `threadsafe: true`. 32 | // 33 | // This is how it work: 34 | // [UserFunction] --> [Bind] --> [Worker] -> [WebUI] 35 | // [WebUI] --> [Worker] --> [ffiWorker.onmessage] --> [UserFunction] 36 | 37 | const ffiWorker = new Worker(new URL("./ffi_worker.ts", import.meta.url).href, { type: "module" }); 38 | const pendingResponses = new Map void; reject: (e: any) => void }>(); 39 | let callbackRegistry: BindCallback[] = []; 40 | let callbackFileHandlerRegistry: BindFileHandlerCallback[] = []; 41 | ffiWorker.onmessage = async (event: MessageEvent) => { 42 | const { id, action, result, error, data } = event.data; 43 | if (action === "invokeCallback") { 44 | const { 45 | callbackIndex, 46 | param_window, 47 | param_event_type, 48 | param_element, 49 | param_event_number, 50 | param_bind_id 51 | } = data; 52 | const callbackFn = callbackRegistry[callbackIndex]; 53 | if (typeof callbackFn !== "undefined") { 54 | 55 | // Window 56 | const win = param_window; 57 | 58 | // Event Type 59 | const event_type = 60 | typeof param_event_type === "bigint" 61 | ? Number(param_event_type) 62 | : Math.trunc(param_event_type); 63 | 64 | // Element 65 | const element = 66 | param_element !== null 67 | ? new CString(param_element) 68 | : ""; 69 | 70 | // Event Number 71 | const event_number = 72 | typeof param_event_number === "bigint" 73 | ? Number(param_event_number) 74 | : Math.trunc(param_event_number); 75 | 76 | // Bind ID 77 | const bind_id = 78 | typeof param_bind_id === "bigint" 79 | ? Number(param_bind_id) 80 | : Math.trunc(param_bind_id); 81 | 82 | // Arguments 83 | const args = { 84 | number: (index: number): number => { 85 | return Number(_lib.symbols.webui_interface_get_int_at(BigInt(win), BigInt(event_number), BigInt(index))); 86 | }, 87 | string: (index: number): string => { 88 | return new CString( 89 | _lib.symbols.webui_interface_get_string_at(BigInt(win), BigInt(event_number), BigInt(index)) 90 | ); 91 | }, 92 | boolean: (index: number): boolean => { 93 | return _lib.symbols.webui_interface_get_bool_at(BigInt(win), BigInt(event_number), BigInt(index)); 94 | }, 95 | }; 96 | 97 | // Calling user callback 98 | const e: WebUIEvent = { 99 | window: windows.get(win)!, 100 | eventType: event_type, 101 | eventNumber: event_number, 102 | element: element, 103 | arg: args, 104 | }; 105 | const result: string = (await callbackFn(e) as string) ?? ''; 106 | 107 | // Set response back to WebUI 108 | _lib.symbols.webui_interface_set_response( 109 | BigInt(win), 110 | BigInt(event_number), 111 | toCString(result), 112 | ); 113 | } 114 | } else if (action === "invokeFileHandler") { 115 | const { 116 | callbackIndex, 117 | windowId, 118 | param_url, 119 | param_length 120 | } = data; 121 | const callbackFileHandlerFn = callbackFileHandlerRegistry[callbackIndex]; 122 | if (typeof callbackFileHandlerFn !== "undefined") { 123 | 124 | // Get URL as string 125 | const url_str :string = param_url !== null ? 126 | new CString(param_url) : ""; 127 | 128 | // Create URL Obj 129 | const url_obj :URL = new URL(url_str, "http://localhost"); 130 | 131 | // Call the user callback 132 | const user_response: string|Uint8Array = await callbackFileHandlerFn(url_obj); 133 | 134 | // We can pass a local buffer to WebUI like this: 135 | // `return user_response;` However, this may create 136 | // a memory leak because WebUI cannot free it, or cause 137 | // memory corruption as Bun may free the buffer before 138 | // WebUI uses it. Therefore, the solution is to create 139 | // a safe WebUI buffer through WebUI API. This WebUI 140 | // buffer will be automatically freed by WebUI later. 141 | const webui_ptr :number = _lib.symbols.webui_malloc(BigInt(user_response.length)); 142 | 143 | // Copy data to C safe buffer 144 | if (typeof user_response === "string") { 145 | // copy `user_response` to `webui_ptr` as String data 146 | const cString = toCString(user_response); 147 | _lib.symbols.webui_memcpy(webui_ptr, cString, BigInt(cString.length)); 148 | } else { 149 | // copy `user_response` to `webui_ptr` as Uint8Array data 150 | _lib.symbols.webui_memcpy(webui_ptr, user_response, BigInt(user_response.length)); 151 | } 152 | 153 | // Send back the response 154 | _lib.symbols.webui_interface_set_response_file_handler( 155 | BigInt(windowId), 156 | webui_ptr, 157 | BigInt(user_response.length), 158 | ); 159 | } 160 | } else if (id) { 161 | const pending = pendingResponses.get(id); 162 | if (pending) { 163 | if (error) pending.reject(error); 164 | else pending.resolve(result); 165 | pendingResponses.delete(id); 166 | } 167 | } 168 | }; 169 | 170 | function postToWorker(message: any): Promise { 171 | return new Promise((resolve, reject) => { 172 | const id = Math.random().toString(36).substring(2) + Date.now().toString(); 173 | message.id = id; 174 | pendingResponses.set(id, { resolve, reject }); 175 | ffiWorker.postMessage(message); 176 | }); 177 | } 178 | 179 | // WebUI Class 180 | 181 | export class WebUI { 182 | #window: Usize = 0; 183 | #lib: WebUILib; 184 | #isFileHandler: boolean = false; 185 | 186 | /** 187 | * Instantiate a new WebUI window. 188 | */ 189 | constructor() { 190 | WebUI.init(); // Init lib if not already initialized 191 | this.#lib = _lib; 192 | this.#window = _lib.symbols.webui_new_window(); 193 | windows.set(BigInt(this.#window), this); 194 | } 195 | 196 | /** 197 | * Set root folder for proper loading resources. 198 | */ 199 | setRootFolder(rootFolder: string) { 200 | const status = this.#lib.symbols.webui_set_root_folder( 201 | BigInt(this.#window), 202 | toCString(rootFolder), 203 | ); 204 | if (!status) { 205 | throw new WebUIError(`unable to set root folder`); 206 | } 207 | } 208 | 209 | /** 210 | * Show the window or update the UI with the new content. 211 | */ 212 | async show(content: string) { 213 | const status = this.#lib.symbols.webui_show( 214 | BigInt(this.#window), 215 | toCString(content), 216 | ); 217 | if (!this.#isFileHandler) { 218 | // Check if window is launched 219 | if (!status) { 220 | throw new WebUIError(`unable to start the browser`); 221 | } 222 | // Wait for window connection (max 30 seconds) 223 | for (let i = 0; i < 120; i++) { 224 | if (!this.isShown) { 225 | await new Promise((resolve) => setTimeout(resolve, 250)); 226 | } else { 227 | break; 228 | } 229 | } 230 | // Check if window is connected 231 | if (!this.isShown) { 232 | throw new WebUIError(`unable to connect to the browser`); 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * Show the window or update the UI with new content using a specific browser. 239 | */ 240 | async showBrowser(content: string, browser: WebUI.Browser) { 241 | const status = this.#lib.symbols.webui_show_browser( 242 | BigInt(this.#window), 243 | toCString(content), 244 | BigInt(browser), 245 | ); 246 | if (!status) { 247 | throw new WebUIError(`unable to start the browser`); 248 | } 249 | for (let i = 0; i < 120; i++) { 250 | if (!this.isShown) { 251 | await new Promise((resolve) => setTimeout(resolve, 250)); 252 | } else { 253 | break; 254 | } 255 | } 256 | if (!this.isShown) { 257 | throw new WebUIError(`unable to connect to the browser`); 258 | } 259 | } 260 | 261 | /** 262 | * Checks if the window is currently running. 263 | */ 264 | get isShown() { 265 | return this.#lib.symbols.webui_is_shown(BigInt(this.#window)); 266 | } 267 | 268 | /** 269 | * Closes the current window. 270 | */ 271 | close() { 272 | return this.#lib.symbols.webui_close(BigInt(this.#window)); 273 | } 274 | 275 | /** 276 | * Execute a JavaScript string in the UI and return a Promise with the client response. 277 | */ 278 | script( 279 | script: string, 280 | options?: { 281 | timeout?: number; 282 | bufferSize?: number; 283 | }, 284 | ) { 285 | const bufferSize = 286 | options?.bufferSize && options.bufferSize > 0 ? options.bufferSize : 1024 * 1000; 287 | const buffer = new Uint8Array(bufferSize); 288 | const timeout = options?.timeout ?? 0; 289 | 290 | const status = this.#lib.symbols.webui_script( 291 | BigInt(this.#window), 292 | toCString(script), 293 | BigInt(timeout), 294 | buffer, 295 | BigInt(bufferSize), 296 | ); 297 | 298 | const response = fromCString(buffer); 299 | 300 | if (status) { 301 | return Promise.resolve(response); 302 | } 303 | return Promise.reject(response); 304 | } 305 | 306 | /** 307 | * Execute a JavaScript string in the UI for a specific client. 308 | */ 309 | scriptClient( 310 | e: WebUIEvent, 311 | script: string, 312 | options?: { 313 | timeout?: number; 314 | bufferSize?: number; 315 | }, 316 | ) { 317 | const bufferSize = 318 | options?.bufferSize && options.bufferSize > 0 ? options.bufferSize : 1024 * 1000; 319 | const buffer = new Uint8Array(bufferSize); 320 | const timeout = options?.timeout ?? 0; 321 | 322 | const status = this.#lib.symbols.webui_interface_script_client( 323 | BigInt(this.#window), 324 | BigInt(e.eventNumber), 325 | toCString(script), 326 | BigInt(timeout), 327 | buffer, 328 | BigInt(bufferSize), 329 | ); 330 | 331 | const response = fromCString(buffer); 332 | 333 | if (status) { 334 | return Promise.resolve(response); 335 | } 336 | return Promise.reject(response); 337 | } 338 | 339 | /** 340 | * Execute a JavaScript string in the UI without waiting for the result. 341 | */ 342 | run(script: string) { 343 | this.#lib.symbols.webui_run( 344 | BigInt(this.#window), 345 | toCString(script), 346 | ); 347 | } 348 | 349 | /** 350 | * Bind a callback function to an HTML element. 351 | */ 352 | bind( 353 | id: string, 354 | callback: BindCallback, 355 | ) { 356 | // Save the user callback function in the registry and obtain its index. 357 | const index = callbackRegistry.push(callback) - 1; 358 | // Send a message to the worker with callback index. 359 | postToWorker({ 360 | action: "bind", 361 | data: { 362 | windowId: this.#window.toString(), 363 | elementId: id, 364 | callbackIndex: index, 365 | }, 366 | }).catch((error) => { 367 | throw new WebUIError("Binding callback failed: " + error); 368 | }); 369 | } 370 | 371 | /** 372 | * Sets a custom files handler to respond to HTTP requests. 373 | */ 374 | setFileHandler(callback: (url: URL) => Promise) { 375 | // C: .show_wait_connection = false; // 0 376 | // Disable `.show()` auto waiting for window connection, 377 | // otherwise `.setFileHandler()` will be blocked. 378 | _lib.symbols.webui_set_config(BigInt(0), false); 379 | 380 | // C: .use_cookies = false; // 4 381 | // We need to disable WebUI Cookies because 382 | // user will use his own custom HTTP header 383 | // in `.setFileHandler()`. 384 | _lib.symbols.webui_set_config(BigInt(4), false); 385 | 386 | // Let `.show()` knows that the user is using `.setFileHandler()` 387 | // so no need to wait for window connection in `.show()`. 388 | this.#isFileHandler = true; 389 | 390 | // Save the user callback function in the registry and obtain its index. 391 | const index = callbackFileHandlerRegistry.push(callback) - 1; 392 | // Send a message to the worker with callback index. 393 | postToWorker({ 394 | action: "setFileHandler", 395 | data: { 396 | windowId: this.#window.toString(), 397 | callbackIndex: index, 398 | }, 399 | }).catch((error) => { 400 | throw new WebUIError("Setting file handler failed: " + error); 401 | }); 402 | } 403 | 404 | /** 405 | * Sets the profile name and path for the current window. 406 | */ 407 | setProfile(name: string, path: string) { 408 | return this.#lib.symbols.webui_set_profile( 409 | BigInt(this.#window), 410 | toCString(name), 411 | toCString(path), 412 | ); 413 | } 414 | 415 | /** 416 | * Set the kiosk mode of a WebUI window. 417 | */ 418 | setKiosk(status: boolean): void { 419 | this.#lib.symbols.webui_set_kiosk(BigInt(this.#window), status); 420 | } 421 | 422 | /** 423 | * Close a specific window and free all memory resources. 424 | */ 425 | destroy(): void { 426 | this.#lib.symbols.webui_destroy(BigInt(this.#window)); 427 | } 428 | 429 | /** 430 | * Set the default embedded HTML favicon. 431 | */ 432 | setIcon(icon: string, iconType: string): void { 433 | this.#lib.symbols.webui_set_icon( 434 | BigInt(this.#window), 435 | toCString(icon), 436 | toCString(iconType), 437 | ); 438 | } 439 | 440 | /** 441 | * Safely send raw data to the UI. 442 | */ 443 | sendRaw(functionName: string, raw: Uint8Array): void { 444 | this.#lib.symbols.webui_send_raw( 445 | BigInt(this.#window), 446 | toCString(functionName), 447 | raw, 448 | BigInt(raw.length), 449 | ); 450 | } 451 | 452 | /** 453 | * Set a window in hidden mode. Should be called before .show(). 454 | */ 455 | setHide(status: boolean): void { 456 | this.#lib.symbols.webui_set_hide(BigInt(this.#window), status); 457 | } 458 | 459 | /** 460 | * Set the window size. 461 | */ 462 | setSize(width: number, height: number): void { 463 | this.#lib.symbols.webui_set_size(BigInt(this.#window), width, height); 464 | } 465 | 466 | /** 467 | * Set the window position. 468 | */ 469 | setPosition(x: number, y: number): void { 470 | this.#lib.symbols.webui_set_position(BigInt(this.#window), x, y); 471 | } 472 | 473 | /** 474 | * Get the full current URL. 475 | */ 476 | getUrl(): string { 477 | return new CString( 478 | this.#lib.symbols.webui_get_url(BigInt(this.#window)) 479 | ); 480 | } 481 | 482 | /** 483 | * Allow the window address to be accessible from a public network. 484 | */ 485 | setPublic(status: boolean): void { 486 | this.#lib.symbols.webui_set_public(BigInt(this.#window), status); 487 | } 488 | 489 | /** 490 | * Navigate to a specific URL. 491 | */ 492 | navigate(url: string): void { 493 | this.#lib.symbols.webui_navigate(BigInt(this.#window), toCString(url)); 494 | } 495 | 496 | /** 497 | * Delete the web-browser local profile folder. 498 | */ 499 | deleteProfile(): void { 500 | this.#lib.symbols.webui_delete_profile(BigInt(this.#window)); 501 | } 502 | 503 | /** 504 | * Get the ID of the parent process. 505 | */ 506 | getParentProcessId(): BigInt { 507 | return this.#lib.symbols.webui_get_parent_process_id(BigInt(this.#window)); 508 | } 509 | 510 | /** 511 | * Get the ID of the last child process. 512 | */ 513 | getChildProcessId(): number { 514 | return Number(this.#lib.symbols.webui_get_child_process_id(BigInt(this.#window))); 515 | } 516 | 517 | /** 518 | * Set a custom web-server network port to be used by WebUI. 519 | */ 520 | setPort(port: number): boolean { 521 | return this.#lib.symbols.webui_set_port(BigInt(this.#window), BigInt(port)); 522 | } 523 | 524 | /** 525 | * Choose between Bun and Node.js as runtime for .js and .ts files. 526 | */ 527 | setRuntime(runtime: number): void { 528 | this.#lib.symbols.webui_set_runtime(BigInt(this.#window), BigInt(runtime)); 529 | } 530 | 531 | /** 532 | * Get the recommended web browser ID to use. 533 | */ 534 | getBestBrowser(): number { 535 | return Number(this.#lib.symbols.webui_get_best_browser(BigInt(this.#window))); 536 | } 537 | 538 | /** 539 | * Start only the web server and return the URL. No window will be shown. 540 | */ 541 | startServer(content: string): string { 542 | return fromCString( 543 | this.#lib.symbols.webui_start_server(BigInt(this.#window), toCString(content)) 544 | ); 545 | } 546 | 547 | /** 548 | * Show a WebView window using embedded HTML or a file. 549 | */ 550 | showWebView(content: string): boolean { 551 | return this.#lib.symbols.webui_show_wv( 552 | BigInt(this.#window), 553 | toCString(content), 554 | ); 555 | } 556 | 557 | /** 558 | * Add a user-defined web browser's CLI parameters. 559 | */ 560 | setCustomParameters(params: string): void { 561 | this.#lib.symbols.webui_set_custom_parameters( 562 | BigInt(this.#window), 563 | toCString(params), 564 | ); 565 | } 566 | 567 | /** 568 | * Set high-contrast support for the window. 569 | */ 570 | setHighContrast(status: boolean): void { 571 | this.#lib.symbols.webui_set_high_contrast(BigInt(this.#window), status); 572 | } 573 | 574 | /** 575 | * Set the window minimum size. 576 | */ 577 | setMinimumSize(width: number, height: number): void { 578 | this.#lib.symbols.webui_set_minimum_size(BigInt(this.#window), width, height); 579 | } 580 | 581 | /** 582 | * Set the web browser proxy server to use. 583 | */ 584 | setProxy(proxyServer: string): void { 585 | this.#lib.symbols.webui_set_proxy( 586 | BigInt(this.#window), 587 | toCString(proxyServer), 588 | ); 589 | } 590 | 591 | // Static methods 592 | 593 | /** 594 | * Get OS high contrast preference. 595 | */ 596 | static isHighContrast(): boolean { 597 | WebUI.init(); 598 | return _lib.symbols.webui_is_high_contrast(); 599 | } 600 | 601 | /** 602 | * Check if a web browser is installed. 603 | */ 604 | static browserExist(browser: WebUI.Browser): boolean { 605 | WebUI.init(); 606 | return _lib.symbols.webui_browser_exist(BigInt(browser)); 607 | } 608 | 609 | /** 610 | * Set the web-server root folder path for all windows. 611 | */ 612 | static setDefaultRootFolder(path: string): boolean { 613 | WebUI.init(); 614 | return _lib.symbols.webui_set_default_root_folder(toCString(path)); 615 | } 616 | 617 | /** 618 | * Open an URL in the native default web browser. 619 | */ 620 | static openUrl(url: string): void { 621 | WebUI.init(); 622 | _lib.symbols.webui_open_url(toCString(url)); 623 | } 624 | 625 | /** 626 | * Get an available free network port. 627 | */ 628 | static getFreePort(): number { 629 | WebUI.init(); 630 | return Number(_lib.symbols.webui_get_free_port()); 631 | } 632 | 633 | /** 634 | * Automatically refresh the window UI when any file in the root folder changes. 635 | */ 636 | static setFolderMonitor(status: boolean): void { 637 | WebUI.init(); 638 | _lib.symbols.webui_set_config(BigInt(2), status); 639 | } 640 | 641 | /** 642 | * Initialize the WebUI library if it's not already initialized. 643 | */ 644 | private static init() { 645 | if (typeof _lib === "undefined") { 646 | _lib = loadLib(); 647 | // Enable asynchronous responses for callbacks. 648 | _lib.symbols.webui_set_config(BigInt(5), true); 649 | } 650 | } 651 | 652 | /** 653 | * Close all opened windows and break WebUI.wait(). 654 | */ 655 | static exit() { 656 | WebUI.init(); 657 | _lib.symbols.webui_exit(); 658 | } 659 | 660 | /** 661 | * Set TLS certificate and private key. 662 | */ 663 | static setTLSCertificate(certificatePem: string, privateKeyPem: string) { 664 | WebUI.init(); 665 | const status = _lib.symbols.webui_set_tls_certificate( 666 | toCString(certificatePem), 667 | toCString(privateKeyPem), 668 | ); 669 | if (!status) { 670 | throw new WebUIError(`unable to set certificate`); 671 | } 672 | } 673 | 674 | /** 675 | * Wait until all opened windows are closed. 676 | */ 677 | static async wait() { 678 | WebUI.init(); 679 | const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); 680 | while (1) { 681 | await sleep(100); 682 | if (!_lib.symbols.webui_interface_is_app_running()) { 683 | break; 684 | } 685 | } 686 | } 687 | 688 | /** 689 | * Allow multiple clients to connect to the same window. 690 | */ 691 | static setMultiClient(allow: boolean): void { 692 | WebUI.init(); 693 | _lib.symbols.webui_set_config(BigInt(3), allow); 694 | } 695 | 696 | /** 697 | * Delete all local web-browser profiles. 698 | */ 699 | static deleteAllProfiles(): void { 700 | WebUI.init(); 701 | _lib.symbols.webui_delete_all_profiles(); 702 | } 703 | 704 | /** 705 | * Base64 encoding. 706 | */ 707 | static encode(str: string): string { 708 | WebUI.init(); 709 | return new CString( 710 | _lib.symbols.webui_encode(toCString(str)) 711 | ); 712 | } 713 | 714 | /** 715 | * Base64 decoding. 716 | */ 717 | static decode(str: string): string { 718 | WebUI.init(); 719 | return new CString( 720 | _lib.symbols.webui_decode(toCString(str)) 721 | ); 722 | } 723 | 724 | /** 725 | * Safely allocate memory using WebUI's memory management. 726 | */ 727 | static malloc(size: number): any { 728 | WebUI.init(); 729 | return _lib.symbols.webui_malloc(BigInt(size)); 730 | } 731 | 732 | /** 733 | * Safely free a memory block allocated by WebUI. 734 | */ 735 | static free(ptr: any): void { 736 | WebUI.init(); 737 | _lib.symbols.webui_free(ptr); 738 | } 739 | 740 | /** 741 | * Set the maximum time (in seconds) to wait for the browser to start. 742 | */ 743 | static setTimeout(second: number): void { 744 | WebUI.init(); 745 | _lib.symbols.webui_set_timeout(BigInt(second)); 746 | } 747 | 748 | /** 749 | * Clean all memory resources. WebUI is not usable after this call. 750 | */ 751 | static clean() { 752 | WebUI.init(); 753 | _lib.symbols.webui_clean(); 754 | } 755 | 756 | static get version() { 757 | return "2.5.2"; 758 | } 759 | } 760 | 761 | // Namespace for additional types and enums. 762 | export namespace WebUI { 763 | export type Event = WebUIEvent; 764 | export enum Browser { 765 | NoBrowser = 0, 766 | AnyBrowser, 767 | Chrome, 768 | Firefox, 769 | Edge, 770 | Safari, 771 | Chromium, 772 | Opera, 773 | Brave, 774 | Vivaldi, 775 | Epic, 776 | Yandex, 777 | ChromiumBased, 778 | } 779 | export enum EventType { 780 | Disconnected = 0, 781 | Connected, 782 | MouseClick, 783 | Navigation, 784 | Callback, 785 | } 786 | } 787 | --------------------------------------------------------------------------------