╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────╮ │19 pub const Title = @import("./ui/Title.zig"); │ │20 pub const SubTitle = @import("./ui/SubTitle.zig"); │ │21 pub const Quit = @import("./ui/Quit.zig"); │ │22 +pub const CloneUrl = @import("./ui/CloneUrl.zig"); │ │23 pub const Unauthorized = @import("./ui/Unauthorized.zig");│ │24 │ │25 pub const PageKind = enum { │ ╰──────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │967 // port the web UI is served on, for the TUI/SSH footer's "http://localhost:<port>..."│ │968 // url. null on the web itself (no footer there). │ │969 web_port: ?u16 = null, │ │970 + // a host operation requested by a widget. terminals present clone urls on │ │971 + // their restored screen; wasm asks the browser to copy them. │ │972 + host_request: ?HostRequest = null, │ │973 quit_requested: bool = false, │ │974 + │ │975 + pub const HostRequest = union(enum) { │ │976 + clone_url: []const u8, │ │977 + }; │ │978 │ │979 const Self = @This(); │ │980 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │988 current_page: RoutablePage = .default, │ │989 // whether to render the ANSI art backdrop │ │990 enable_ansi: bool = true, │ │991 + // the bound clone-service ports. absent in local mode, where clone urls│ │992 + // are not shown. │ │993 + clone_http_port: ?u16 = null, │ │994 + clone_ssh_port: ?u16 = null, │ │995 // true when this session views a single local repo. unlike `local` │ │996 // (the host-side filesystem source) this travels in the snapshot, so │ │997 // the wasm side also parses elided link urls and hides the multi-user │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────╮ │1109 defer nav.deinit(allocator); │ │1110 │ │1111 var terminal = try term.Terminal.init(io, allocator);│ │1099 - defer terminal.deinit(io); │ │1112 + var terminal_live = true; │ │1113 + defer if (terminal_live) terminal.deinit(io); │ │1114 │ │1115 // set term as active so it will be properly cooked │ │1116 // when a panic/segfault happens │ ╰───────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │1140 try inputKey(allocator, &nav.root, key, session); │ │1141 } │ │1142 │ │1143 + if (session.host_request) |request| { │ │1144 + session.host_request = null; │ │1145 + switch (request) { │ │1146 + .clone_url => |url| { │ │1147 + term.setActive(null); │ │1148 + terminal.deinit(io); │ │1149 + terminal_live = false; │ │1150 + showCloneUrl(io, url) catch |show_err| { │ │1151 + terminal = try term.Terminal.init(io, allocator); │ │1152 + terminal_live = true; │ │1153 + term.setActive(&terminal); │ │1154 + return show_err; │ │1155 + }; │ │1156 + terminal = try term.Terminal.init(io, allocator); │ │1157 + terminal_live = true; │ │1158 + term.setActive(&terminal); │ │1159 + last_size = .{ .width = 0, .height = 0 }; │ │1160 + }, │ │1161 + } │ │1162 + } │ │1163 + │ │1164 // persist queued actions when there's a repo; local mode has none,│ │1165 // so just apply in-memory │ │1166 if (repo_maybe) |repo| { │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │1181 .min_size = .{ .width = null, .height = null }, │ │1182 .max_size = .{ .width = last_size.width, .height = last_size.height }, │ │1183 }, nav.root.getFocus()); │ │1184 + } │ │1185 +} │ │1186 + │ │1187 +fn showCloneUrl(io: std.Io, url: []const u8) !void { │ │1188 + const tty: ?std.Io.File = if (builtin.os.tag == .windows) │ │1189 + null │ │1190 + else │ │1191 + try std.Io.Dir.cwd().openFile(io, "/dev/tty", .{ .mode = .read_write }); │ │1192 + defer if (tty) |file| file.close(io); │ │1193 + │ │1194 + const input = if (tty) |file| file else std.Io.File.stdin(); │ │1195 + const output = if (tty) |file| file else std.Io.File.stdout(); │ │1196 + │ │1197 + var write_buf: [1024]u8 = undefined; │ │1198 + var writer = output.writer(io, &write_buf); │ │1199 + try writer.interface.print("\x1b[2J\x1b[H{s}\r\n\r\npress enter to go back\r\n", .{url});│ │1200 + try writer.interface.flush(); │ │1201 + │ │1202 + var read_buf: [1]u8 = undefined; │ │1203 + var reader = input.reader(io, &read_buf); │ │1204 + while (true) { │ │1205 + const byte = try reader.interface.takeByte(); │ │1206 + if (byte == '\r' or byte == '\n') return; │ │1207 } │ │1208 } │ │1209 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────╮ │1568 repo_refs: Repo.Refs.View, │ │1569 repo_issues: Repo.Issues.View, │ │1570 repo_comment: Repo.Comment.Item, │ │1571 + clone_url: CloneUrl.View, │ │1572 home_users: Home.Users.View, │ │1573 home_repos: Home.Repos.View, │ │1574 auth_tab: Home.Header.AuthTab.View,│ ╰─────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │359 } │ │360 │ │361 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void {│ │362 + const stack = &self.box.children.values()[stack_index].widget.stack; │ │363 if (self.getFocus().child_id) |child_id| { │ │364 if (self.box.children.getIndex(child_id)) |current_index| { │ │365 const child = &self.box.children.values()[current_index].widget; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │398 .down => { │ │399 switch (child.*) { │ │400 .repo_header => { │ │401 + if (stack.getSelected()) |selected_widget| switch (selected_widget.*) {│ │402 + .repo_files => |*v| if (v.focusCloneUrl(root_focus)) return, │ │403 + .repo_commits => |*v| if (v.focusCloneUrl(root_focus)) return, │ │404 + else => {}, │ │405 + }; │ │406 index = stack_index; │ │407 }, │ │408 .stack => { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │429 │ │430 // the ref banner at the top. │ │431 { │ │432 - var header = try Header.View.init(allocator, &data.header, session); │ │433 - errdefer header.deinit(allocator); │ │434 - try outer.children.put(allocator, header.getFocus().id, .{ .widget = .{ .repo_commits_header = header }, .rect = null, .min_size = .{ .width = null, .height = 3 } }); │ │432 + var header_view = try Header.View.init(allocator, &data.header, session, data.identity); │ │433 + errdefer header_view.deinit(allocator); │ │434 + try outer.children.put(allocator, header_view.getFocus().id, .{ .widget = .{ .repo_commits_header = header_view }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │435 } │ │436 │ │437 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯