╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ web.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
127 } 128 return try out.toOwnedSlice(allocator); 129 } 130 131 fn handleConnection( 132 io: std.Io, 133 allocator: std.mem.Allocator, 134 stream: std.Io.net.Stream, 135 admin_repo_path: []const u8, 136 err: *std.Io.Writer, 137 ) !void { 138 var send_buffer = [_]u8{0} ** 4096; 139 var recv_buffer = [_]u8{0} ** 4096; 140 var conn_br = stream.reader(io, &recv_buffer); 141 var conn_bw = stream.writer(io, &send_buffer); 142 var http_server = std.http.Server.init(&conn_br.interface, &conn_bw.interface); 143 144 while (http_server.reader.state == .ready) { 145 var request = http_server.receiveHead() catch |receive_err| switch (receive_err) { 146 error.HttpConnectionClosing => break, 147 error.ReadFailed => break, 148 else => |e| return e, 149 }; 150 151 handleRequest(io, &http_server, &request, allocator, admin_repo_path) catch |request_err| { 152 try err.print("web ui request failed: {s}\n", .{@errorName(request_err)}); 153 try err.flush(); 154 if (http_server.reader.state == .received_head) { 155 http_server.reader.state = .ready; 156 } 157 try writeSimpleResponse(&http_server, 500, "Internal Server Error", "text/plain", @errorName(request_err)); 158 }; 159 try http_server.out.flush(); 160 break; 161 } 162 } 163 164 fn handleRequest( 165 io: std.Io, 166 http_server: *std.http.Server, 167 request: *std.http.Server.Request, 168 allocator: std.mem.Allocator, 169 admin_repo_path: []const u8, 170 ) !void { 171 const method = if (request.head.method == .HEAD) .GET else request.head.method; 172 if (method != .GET) { 173 if (http_server.reader.state == .received_head) { 174 http_server.reader.state = .ready; 175 } 176 try writeSimpleResponse(http_server, 405, "Method Not Allowed", "text/plain", "method not allowed"); 177 return; 178 } 179 180 const uri = try std.Uri.parseAfterScheme("", request.head.target); 181 const path = uri.path.percent_encoded; 182 183 if (std.mem.eql(u8, path, "/favicon.ico")) { 184 if (http_server.reader.state == .received_head) { 185 http_server.reader.state = .ready; 186 } 187 try writeStaticResponse(http_server, 204, "No Content", "image/x-icon", "", true); 188 return; 189 } 190 191 const embed = findEmbed(path) orelse { 192 if (http_server.reader.state == .received_head) { 193 http_server.reader.state = .ready; 194 } 195 try writeSimpleResponse(http_server, 404, "Not Found", "text/plain", "not found"); 196 return; 197 }; 198 199 if (http_server.reader.state == .received_head) { 200 http_server.reader.state = .ready; 201 } 202 203 if (std.mem.eql(u8, embed.path, "index.html")) { 204 const index_html = try renderIndexHtml(io, allocator, admin_repo_path); 205 defer allocator.free(index_html); 206 try writeStaticResponse(http_server, 200, "OK", embed.content_type, index_html, request.head.method == .HEAD); 207 } else { 208 try writeStaticResponse(http_server, 200, "OK", embed.content_type, embed.body, request.head.method == .HEAD); 209 } 210 } 211 212 fn findEmbed(request_path: []const u8) ?Embed { 213 const path = if (std.mem.eql(u8, request_path, "/")) 214 "index.html" 215 else if (request_path.len > 1 and request_path[0] == '/') 216 request_path[1..] 217 else 218 return null; 219 220 for (embeds) |embed| { 221 if (std.mem.eql(u8, path, embed.path)) return embed; 222 } 223 return null; 224 } 225 226 fn writeSimpleResponse( 227 http_server: *std.http.Server, 228 code: u16, 229 message: []const u8, 230 content_type: []const u8, 231 body: []const u8, 232 ) !void { 233 try http_server.out.print( 234 "HTTP/1.1 {d} {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\n\r\n{s}", 235 .{ code, message, content_type, body.len, body }, 236 ); 237 } 238 239 fn writeStaticResponse( 240 http_server: *std.http.Server, 241 code: u16, 242 message: []const u8, 243 content_type: []const u8, 244 body: []const u8, 245 head_only: bool, 246 ) !void { 247 try http_server.out.print( 248 "HTTP/1.1 {d} {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\n\r\n", 249 .{ code, message, content_type, body.len }, 250 ); 251 if (!head_only) { 252 try http_server.out.writeAll(body); 253 } 254 } 255 256 fn logError(err: *std.Io.Writer, comptime fmt: []const u8, args: anytype) void { 257 err.print(fmt, args) catch return; 258 err.flush() catch {}; 259 } 260 261 pub fn generateHtml(allocator: std.mem.Allocator, root: *ui.Widget) ![]const u8 { 262 const grid = root.getGrid() orelse return error.MissingGrid; 263 const root_focus = root.getFocus(); 264 265 var out: std.ArrayList(u8) = .empty; 266 errdefer out.deinit(allocator); 267 268 for (0..grid.size.height) |y| { 269 // wrap runs of cells belonging to a focusable widget in a span tagged 270 // with that widget's focus id. CSS paints them with a pointer cursor; 271 // JS reads the id on click and dispatches focus directly. 272 var current_id: ?usize = null; 273 for (0..grid.size.width) |x| { 274 const cell_id = cellFocusId(root_focus, x, y); 275 if (cell_id != current_id) { 276 if (current_id != null) try out.appendSlice(allocator, "</span>"); 277 if (cell_id) |id| { 278 var buf: [64]u8 = undefined; 279 const tag = try std.fmt.bufPrint(&buf, "<span class=\"clickable\" data-focus-id=\"{}\">", .{id}); 280 try out.appendSlice(allocator, tag); 281 } 282 current_id = cell_id; 283 } 284 const rune = grid.cells.items[try grid.cells.at(.{ y, x })].rune orelse " "; 285 try appendEscapedHtml(allocator, &out, rune); 286 } 287 if (current_id != null) try out.appendSlice(allocator, "</span>"); 288 try out.append(allocator, '\n'); 289 } 290 291 return try out.toOwnedSlice(allocator); 292 } 293 294 fn cellFocusId(focus: *Focus, x: usize, y: usize) ?usize { 295 var iter = focus.children.iterator(); 296 while (iter.next()) |entry| { 297 const child = entry.value_ptr.*; 298 if (!child.focus.focusable) continue; 299 const r = child.rect; 300 if (x >= r.x and y >= r.y and x < r.x + r.size.width and y < r.y + r.size.height) { 301 return entry.key_ptr.*; 302 } 303 } 304 return null; 305 } 306 307 fn appendEscapedHtml(allocator: std.mem.Allocator, out: *std.ArrayList(u8), input: []const u8) !void { 308 for (input) |ch| { 309 switch (ch) { 310 '&' => try out.appendSlice(allocator, "&amp;"), 311 '<' => try out.appendSlice(allocator, "&lt;"), 312 '>' => try out.appendSlice(allocator, "&gt;"), 313 '"' => try out.appendSlice(allocator, "&quot;"), 314 '\'' => try out.appendSlice(allocator, "&#39;"), 315 else => try out.append(allocator, ch), 316 } 317 } 318 } 319