Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new APIs and example #58

Merged
merged 8 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,32 @@ pub fn build(b: *Build) !void {
12, 13, 14 => try V0_12.build(b),
else => @compileError("unknown version!"),
}

// // I (@sensiblearts) used this during development to save copying time.
// // You can remove it if you wish.
// copy_free_port_example_to_subfolder() catch |err| {
// std.debug.print("failed to copy free port example files to ./zig-out/bin: {}", .{err});
// };
}

// fn copy_free_port_example_to_subfolder() !void {
// var src_dir = try std.fs.cwd().openDir("./examples/custom_spa_server_on_free_port", .{});
// var dest_dir = try std.fs.cwd().makeOpenPath("./zig-out/bin/custom_spa_server_on_free_port", .{});
// defer dest_dir.close();
// try src_dir.copyFile("index.html", dest_dir, "index.html", .{});
// try src_dir.copyFile("pages.js", dest_dir, "pages.js", .{});
// try src_dir.copyFile("free_port_web_server.py", dest_dir, "free_port_web_server.py", .{});
// src_dir.close();
// src_dir = try std.fs.cwd().openDir("./zig-out/bin", .{});
// defer src_dir.close();
// // You have to run 'zig build' TWICE because these don't exist the first time:
// try src_dir.copyFile("custom_spa_server_on_free_port.exe", dest_dir, "custom_spa_server_on_free_port.exe", .{});
// try src_dir.copyFile("custom_spa_server_on_free_port.pdb", dest_dir, "custom_spa_server_on_free_port.pdb", .{});
// // Could probably use b.getInstallStep().dependOn(&b.addInstallFileWithDir(..., .prefix, ...).step);
// // but I have not figured that out yet.
//
// }

/// build for zig 0.12
pub const V0_12 = struct {
const OptimizeMode = std.builtin.OptimizeMode;
Expand Down
4 changes: 2 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
.minimum_zig_version = "0.11.0",
.dependencies = .{
.webui = .{
.url = "https://github.com/webui-dev/webui/archive/50e9e4c2c6358504d651310fa97e4e8be8c8f42c.tar.gz",
.hash = "1220184e5dff426303909da4c9333450e890c366b2624f071bedf86441f3a16dfbf3",
.url = "https://github.com/webui-dev/webui/archive/bdd89e22ea13878e1bd99dd847ddfca385cb38e5.tar.gz",
.hash = "1220812dbd5e12f6673f2b3cd6e654989874850bcdcbb8a5d8f908688f3b34a4da53",
},
},
.paths = .{
Expand Down
33 changes: 33 additions & 0 deletions examples/custom_spa_server_on_free_port/free_port_web_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import http.server
from http.server import BaseHTTPRequestHandler
import socketserver
import sys

SERVER_PORT_STR = sys.argv[1]
SERVER_PORT = int(SERVER_PORT_STR)

WEBUI_PORT_STR = sys.argv[2]

class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
try:
f = open("." + self.path)
content = f.read()
self.send_response(200)
if (self.path.endswith(".html")):
self.send_header('Content-type','text/html')
# now set webui port
content = content.replace("[WEBUI_PORT]", WEBUI_PORT_STR)
if (self.path.endswith(".js")):
self.send_header('Content-type','text/javascript')
self.send_header('cache-control', 'no-cache')
self.end_headers()
self.wfile.write(content.encode())
f.close()
return
except IOError:
self.send_error(404,'File Not Found: %s' % self.path)

with socketserver.TCPServer(("", SERVER_PORT), MyHandler) as httpd:
print(f"Server started at http://localhost:{SERVER_PORT_STR}")
httpd.serve_forever()
46 changes: 46 additions & 0 deletions examples/custom_spa_server_on_free_port/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="cache-control" content="no-cache" />
<title>WebUI - Custom Web-Server Free Port Example (C)</title>
<!-- Connect this window to the back-end app -->
<script src="http://localhost:[WEBUI_PORT]/webui.js"></script>
<script src="pages.js"></script>
<script>
// called by main.zig
function doNavigate(path) {
window.router(path);
}
</script>
</head>
<body>
<header style="margin-bottom: 20px;">
<div style="flex: row;";>
<a href="index.html"><h2>Home</h2></a>
<button onclick="window.router(window.pages[0].path)"><h3>Second Page</h3></button>
<button onclick="window.router(window.pages[1].path)"><h3>Third Page</h3></button>
<button onclick="gotoPage(window.pages[2].path);">
<!-- calls into main.zig-->
<h2>
Fourth page (JS->Zig->JS)
</h2>
</button>
</div>
</header>
<hr/>
<div id="page" style="border-style: double; padding:20px;";>
<h1>This is the First (Home) Page</h1>
</div>
<hr/>
<footer style="margin-top: 50px;">
<h3>Example: Web-Server that finds available free port (C)</h3>
<p>
Similar to the Custom Web-Server example, this HTML page is handled by a custom Web-Server other than WebUI.<br />
This window is connected to the back-end because we used: <pre>http://localhost:[WEBUI_PORT]/webui.js</pre>
<strong>An available free port ([WEBUI_PORT]) was obtained by calling <pre>webui.getFreePort();</pre>
</strong>
</p>
</footer>
</body>
</html>
146 changes: 146 additions & 0 deletions examples/custom_spa_server_on_free_port/main.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//! Custom web Server - Free Port - Example
const std = @import("std");
const webui = @import("webui");

var gpa = std.heap.GeneralPurposeAllocator(.{}){};

var python_server_proc: std.process.Child = undefined;
var python_running: bool = false;

pub fn main() !void {
// Create new window
var nwin = webui.newWindow();

// Bind all events
_ = nwin.bind("", events);
// Bind a JS call to a Zig fn
_ = nwin.bind("gotoPage", goto_page);

// The `webui.js` script will be available at:
//
// http://localhost:[WEBUI_PORT]/webui.js
//
// (see [WEBUI_PORT] in: index.html, free_port_web_server.py)
//
// So, get and set a free port for WebUI to use:

const webui_port: u64 = webui.getFreePort();
std.debug.print("Free Port for webui.js: {d} \n", .{webui_port});
// now use the port:
_ = nwin.setPort(webui_port);

const backend_port = webui.getFreePort();
std.debug.print("Free Port for custom web server: {d} \n", .{backend_port});
// now use the port:
var buf1: [64]u8 = undefined;
var buf2: [64]u8 = undefined;
const port_argument1: []u8 = try std.fmt.bufPrintZ(&buf1, "{d}", .{backend_port});
const port_argument2: []u8 = try std.fmt.bufPrintZ(&buf2, "{d}", .{webui_port});
const argv = [_][]const u8{ "python", "./free_port_web_server.py", port_argument1, port_argument2 };
python_server_proc = std.process.Child.init(&argv, std.heap.page_allocator);

// start the SPA web server:
startPythonWebServer();

// Show a new window served by our custom web server (spawned above):
var buf: [64]u8 = undefined;
const url: [:0]u8 = try std.fmt.bufPrintZ(&buf, "http://localhost:{d}/index.html", .{backend_port});
_ = nwin.show(url);

// Wait until all windows get closed
webui.wait();

// Free all memory resources (Optional)
webui.clean();

// Free the spawned proc, port and memory
killPythonWebServer();
}

fn startPythonWebServer() void {
if (python_running == false) { // a better check would be a test for the process itself
if (python_server_proc.spawn()) |_| {
python_running = true;
std.debug.print("Spawned python server process PID={}\n", .{python_server_proc.id});
} else |err| {
std.debug.print("NOT Starting python server: {}\n", .{err});
}
}
}

fn killPythonWebServer() void {
if (python_running == true) {
if (python_server_proc.kill()) |_| {
python_running = false;
std.debug.print("Killing python server\n", .{});
} else |err| {
std.debug.print("NOT Killing python server: {}\n", .{err});
}
}
}

// This is a Zig function that is invoked by a Javascript call,
// and in turn, calls Javascript.
fn goto_page(e: webui.Event) void {
// JavaScript that invoked this function: gotoPage('some-path');
const path = e.getString();
std.debug.print("JS invoked Zig: Navigating to page: {s}\n", .{path});
// Now, write a Javascript call to do the navigation:
var js: [64]u8 = std.mem.zeroes([64]u8);
const buf = std.fmt.bufPrint(&js, "doNavigate('{s}');", .{path}) catch unreachable;
// convert it to a Sentinel-Terminated slice
const content: [:0]const u8 = js[0..buf.len :0];
std.debug.print("Zig calling JS: {s}\n", .{buf});
// Run the JavaScript
e.getWindow().run(content);
}

fn events(e: webui.Event) void {
// This function gets called every time
// there is an event
switch (e.event_type) {
.EVENT_CONNECTED => {
std.debug.print("Connected. \n", .{});
},
.EVENT_DISCONNECTED => {
std.debug.print("Disconnected. \n", .{});
},
.EVENT_MOUSE_CLICK => {
std.debug.print("Click. \n", .{});
},
.EVENT_NAVIGATION => {
const allocator = gpa.allocator();

defer {
const deinit_status = gpa.deinit();

if (deinit_status == .leak) @panic("memory leak!");
}

// get the url string
const url = e.getString();
// get the len of url
const len = url.len;

// we use this to get widnow
var tmp_e = e;
var win = tmp_e.getWindow();

// we generate the new url!
const new_url = allocator.allocSentinel(u8, len, 0) catch unreachable;
defer allocator.free(new_url);

std.debug.print("Starting navigation to: {s}\n", .{url});

@memcpy(new_url[0..len], url);

// Because we used `bind(MyWindow, "", events);`
// WebUI will block all `href` link clicks and sent here instead.
// We can then control the behaviour of links as needed.
win.navigate(new_url);
},
else => {
std.debug.print("Other event {}. \n", .{e.event_type});
},
}
}
28 changes: 28 additions & 0 deletions examples/custom_spa_server_on_free_port/pages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
window.pages = [
{
path: '/second-page',
title: 'Second Page',
html: '<h1>This is the SECOND page</h1><br/><a href="index.html">home</a>'
},
{
path: '/third-page',
title: 'Third Page',
html: '<h1>This is the THIRD page</h1><br/><a href="index.html">home</a>'
},
{
path: '/fourth-page',
title: 'Fourth Page',
html: '<h1>This is the FOURTH page</h1><br/><a href="index.html">home</a>'
},
]
const pages = window.pages;

const router = function(path) {
pages.forEach(route => {
if(route.path === path) {
window.history.pushState({}, route.title, path)
document.getElementById('page').innerHTML = route.html
}
})
}
window.router = router
20 changes: 19 additions & 1 deletion src/webui.zig
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ pub fn setDefaultRootFolder(path: [:0]const u8) bool {
return WebUI.webui_set_default_root_folder(@ptrCast(path.ptr));
}

/// Set a custom handler to serve files.
/// Set a custom handler to serve files. This custom handler should
/// return full HTTP header and body.
pub fn setFileHandler(self: Self, comptime handler: fn (filename: []const u8) ?[]const u8) void {
const tmp_struct = struct {
fn handle(tmp_filename: [*c]const u8, length: [*c]c_int) callconv(.C) ?*const anyopaque {
Expand Down Expand Up @@ -317,6 +318,12 @@ pub fn getChildProcessId(self: Self) usize {
return WebUI.webui_get_child_process_id(self.window_handle);
}

/// Get the network port of a running window.
/// This can be useful to determine the HTTP link of `webui.js`
pub fn getPort(self: Self) usize {
return WebUI.webui_get_port(self.window_handle);
}

/// Set a custom web-server network port to be used by WebUI.
/// This can be useful to determine the HTTP link of `webui.js` in case
/// you are trying to use WebUI with an external web-server like NGNIX
Expand All @@ -325,6 +332,11 @@ pub fn setPort(self: Self, port: usize) bool {
return WebUI.webui_set_port(self.window_handle, port);
}

// Get an available usable free network port.
pub fn getFreePort() usize {
return WebUI.webui_get_free_port();
}

/// Control the WebUI behaviour. It's recommended to be called at the beginning.
pub fn setConfig(option: Config, status: bool) void {
WebUI.webui_set_config(@intCast(@intFromEnum(option)), status);
Expand All @@ -338,6 +350,12 @@ pub fn setEventBlocking(self: Self, status: bool) void {
WebUI.webui_set_event_blocking(self.window_handle, status);
}

/// Get the HTTP mime type of a file.
pub fn getMimeType(file: [:0]const u8) []const u8 {
const res = WebUI.webui_get_mime_type(@ptrCast(file.ptr));
return res[0..tools.str_len(res)];
}

/// Set the SSL/TLS certificate and the private key content,
/// both in PEM format.
/// This works only with `webui-2-secure` library.
Expand Down