╭ web.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const ui = @import("./ui.zig"); 3 4 const Asset = struct { 5 path: []const u8, 6 content_type: []const u8, 7 body: []const u8, 8 }; 9 10 const assets = [_]Asset{ 11 .{ .path = "index.html", .content_type = "text/html; charset=utf-8", .body = @embedFile("assets/index.html") }, 12 .{ .path = "script.js", .content_type = "text/javascript; charset=utf-8", .body = @embedFile("assets/script.js") }, 13 .{ .path = "term.ttf", .content_type = "font/ttf", .body = @embedFile("assets/term.ttf") }, 14 .{ .path = "haxy.wasm", .content_type = "application/wasm", .body = @embedFile("assets/haxy.wasm") }, 15 }; 16 17 pub fn run( 18 io: std.Io, 19 allocator: std.mem.Allocator, 20 net_server: *std.Io.net.Server, 21 tasks: *std.Io.Group, 22 err: *std.Io.Writer, 23 ) void { 24 const Listener = struct { 25 io: std.Io, 26 allocator: std.mem.Allocator, 27 net_server: *std.Io.net.Server, 28 tasks: *std.Io.Group, 29 err: *std.Io.Writer, 30 31 fn run(ctx: @This()) void { 32 while (true) { 33 const stream = ctx.net_server.accept(ctx.io) catch |accept_err| { 34 if (accept_err == error.Canceled) return; 35 logError(ctx.err, "web ui accept failed: {s}\n", .{@errorName(accept_err)}); 36 continue; 37 }; 38 39 const Connection = struct { 40 io: std.Io, 41 allocator: std.mem.Allocator, 42 stream: std.Io.net.Stream, 43 err: *std.Io.Writer, 44 45 fn run(conn: @This()) void { 46 defer conn.stream.close(conn.io); 47 handleConnection(conn.io, conn.allocator, conn.stream, conn.err) catch |request_err| { 48 logError(conn.err, "web ui request failed: {s}\n", .{@errorName(request_err)}); 49 }; 50 } 51 }; 52 53 ctx.tasks.async(ctx.io, Connection.run, .{Connection{ 54 .io = ctx.io, 55 .allocator = ctx.allocator, 56 .stream = stream, 57 .err = ctx.err, 58 }}); 59 } 60 } 61 }; 62 63 tasks.async(io, Listener.run, .{Listener{ 64 .io = io, 65 .allocator = allocator, 66 .net_server = net_server, 67 .tasks = tasks, 68 .err = err, 69 }}); 70 } 71 72 fn renderIndexHtml(allocator: std.mem.Allocator) ![]const u8 { 73 const template = (findAsset("/index.html") orelse return error.MissingIndexAsset).body; 74 75 var root = try ui.initRoot(allocator); 76 defer root.deinit(); 77 78 const content = try ui.generateHtml(allocator, &root); 79 defer allocator.free(content); 80 81 const needle = "{{{ HAXY_CONTENT }}}"; 82 const index = std.mem.indexOf(u8, template, needle) orelse return error.MissingTemplateToken; 83 84 var out: std.ArrayList(u8) = .empty; 85 errdefer out.deinit(allocator); 86 try out.appendSlice(allocator, template[0..index]); 87 try out.appendSlice(allocator, content); 88 try out.appendSlice(allocator, template[index + needle.len ..]); 89 return try out.toOwnedSlice(allocator); 90 } 91 92 fn handleConnection( 93 io: std.Io, 94 allocator: std.mem.Allocator, 95 stream: std.Io.net.Stream, 96 err: *std.Io.Writer, 97 ) !void { 98 var send_buffer = [_]u8{0} ** 4096; 99 var recv_buffer = [_]u8{0} ** 4096; 100 var conn_br = stream.reader(io, &recv_buffer); 101 var conn_bw = stream.writer(io, &send_buffer); 102 var http_server = std.http.Server.init(&conn_br.interface, &conn_bw.interface); 103 104 while (http_server.reader.state == .ready) { 105 var request = http_server.receiveHead() catch |receive_err| switch (receive_err) { 106 error.HttpConnectionClosing => break, 107 error.ReadFailed => break, 108 else => |e| return e, 109 }; 110 111 handleRequest(&http_server, &request, allocator) catch |request_err| { 112 try err.print("web ui request failed: {s}\n", .{@errorName(request_err)}); 113 try err.flush(); 114 if (http_server.reader.state == .received_head) { 115 http_server.reader.state = .ready; 116 } 117 try writeSimpleResponse(&http_server, 500, "Internal Server Error", "text/plain", @errorName(request_err)); 118 }; 119 try http_server.out.flush(); 120 break; 121 } 122 } 123 124 fn handleRequest( 125 http_server: *std.http.Server, 126 request: *std.http.Server.Request, 127 allocator: std.mem.Allocator, 128 ) !void { 129 const method = if (request.head.method == .HEAD) .GET else request.head.method; 130 if (method != .GET) { 131 if (http_server.reader.state == .received_head) { 132 http_server.reader.state = .ready; 133 } 134 try writeSimpleResponse(http_server, 405, "Method Not Allowed", "text/plain", "method not allowed"); 135 return; 136 } 137 138 const uri = try std.Uri.parseAfterScheme("", request.head.target); 139 const path = uri.path.percent_encoded; 140 141 if (std.mem.eql(u8, path, "/favicon.ico")) { 142 if (http_server.reader.state == .received_head) { 143 http_server.reader.state = .ready; 144 } 145 try writeStaticResponse(http_server, 204, "No Content", "image/x-icon", "", true); 146 return; 147 } 148 149 const asset = findAsset(path) orelse { 150 if (http_server.reader.state == .received_head) { 151 http_server.reader.state = .ready; 152 } 153 try writeSimpleResponse(http_server, 404, "Not Found", "text/plain", "not found"); 154 return; 155 }; 156 157 if (http_server.reader.state == .received_head) { 158 http_server.reader.state = .ready; 159 } 160 161 if (std.mem.eql(u8, asset.path, "index.html")) { 162 const index_html = try renderIndexHtml(allocator); 163 defer allocator.free(index_html); 164 try writeStaticResponse(http_server, 200, "OK", asset.content_type, index_html, request.head.method == .HEAD); 165 } else { 166 try writeStaticResponse(http_server, 200, "OK", asset.content_type, asset.body, request.head.method == .HEAD); 167 } 168 } 169 170 fn findAsset(request_path: []const u8) ?Asset { 171 const path = if (std.mem.eql(u8, request_path, "/")) 172 "index.html" 173 else if (request_path.len > 1 and request_path[0] == '/') 174 request_path[1..] 175 else 176 return null; 177 178 for (assets) |asset| { 179 if (std.mem.eql(u8, path, asset.path)) return asset; 180 } 181 return null; 182 } 183 184 fn writeSimpleResponse( 185 http_server: *std.http.Server, 186 code: u16, 187 message: []const u8, 188 content_type: []const u8, 189 body: []const u8, 190 ) !void { 191 try http_server.out.print( 192 "HTTP/1.1 {d} {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\n\r\n{s}", 193 .{ code, message, content_type, body.len, body }, 194 ); 195 } 196 197 fn writeStaticResponse( 198 http_server: *std.http.Server, 199 code: u16, 200 message: []const u8, 201 content_type: []const u8, 202 body: []const u8, 203 head_only: bool, 204 ) !void { 205 try http_server.out.print( 206 "HTTP/1.1 {d} {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\n\r\n", 207 .{ code, message, content_type, body.len }, 208 ); 209 if (!head_only) { 210 try http_server.out.writeAll(body); 211 } 212 } 213 214 fn logError(err: *std.Io.Writer, comptime fmt: []const u8, args: anytype) void { 215 err.print(fmt, args) catch return; 216 err.flush() catch {}; 217 } 218