╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │1317 if (crossPageLink(root_focus, gid, session.data)) |route| { │ │1318 return session.navigate(route); │ │1319 } │ │1320 + if (rawLink(root_focus, gid)) |url| return requestRawLink(session, url);│ │1321 } │ │1322 try root.input(allocator, key, root_focus); │ │1323 }, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │1341 // follow a cross-page link │ │1342 if (crossPageLink(root_focus, focus_id, session.data)) |route| {│ │1343 return session.navigate(route); │ │1344 + } │ │1345 + if (rawLink(root_focus, focus_id)) |url| { │ │1346 + if (!mouse.ctrl) try requestRawLink(session, url); │ │1347 + return; │ │1348 } │ │1349 root_focus.setFocus(focus_id); │ │1350 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1360 } │ │1361 } │ │1362 │ │1363 +// a link to bytes the server serves directly rather than to a page route, so │ │1364 +// it never resolves to a RoutablePage │ │1365 +pub const raw_link_prefix = "ax:"; │ │1366 + │ │1367 +// a button the web overlay covers with a file picker that posts the chosen │ │1368 +// file to the url after the prefix │ │1369 +pub const file_input_prefix = "file:"; │ │1370 + │ │1371 +// the url an `ax:` link points at, or null for anything else. a host that │ │1372 +// suppresses the anchor's own navigation needs this to follow the link. │ │1373 +pub fn rawLink(root_focus: *Focus, focus_id: usize) ?[]const u8 { │ │1374 + const child = root_focus.children.get(focus_id) orelse return null; │ │1375 + const custom = switch (child.focus.kind) { │ │1376 + .custom => |c| c, │ │1377 + else => return null, │ │1378 + }; │ │1379 + if (!std.mem.startsWith(u8, custom, raw_link_prefix)) return null; │ │1380 + return custom[raw_link_prefix.len..]; │ │1381 +} │ │1382 + │ │1383 +fn requestRawLink(session: *Session, url: []const u8) !void { │ │1384 + session.host_request = .{ .show_copyable_text = try terminalWebUrl(session.page_arena.allocator(), session, url) };│ │1385 +} │ │1386 + │ │1387 +fn terminalWebUrl(allocator: std.mem.Allocator, session: *const Session, path: []const u8) ![]const u8 { │ │1388 + return if (session.web_port) |port| │ │1389 + try std.fmt.allocPrint(allocator, "http://localhost:{d}{s}", .{ port, path }) │ │1390 + else │ │1391 + path; │ │1392 +} │ │1393 + │ │1394 // if the focus target at focus_id is an `a:` link that should navigate (a │ │1395 // different parent page, or a different files directory within the repo page), │ │1396 // return its route; otherwise null. lets a host turn a click / enter on such a │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │2293 self.url_width = 0; │ │2294 const aa = self.arena.allocator(); │ │2295 const path = self.session.data.current_page.toUrl(&self.arena) catch return; │ │2260 - const text = if (self.session.web_port) |port| │ │2261 - std.fmt.allocPrint(aa, "http://localhost:{d}{s}", .{ port, path }) catch return│ │2262 - else │ │2263 - path; │ │2296 + const text = terminalWebUrl(aa, self.session, path) catch return; │ │2297 self.url = text; │ │2298 │ │2299 var grid = try Grid.init(allocator, .{ .width = width, .height = 1 }); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯