╭ serve_ssh_tui.zig ────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 //! server-side handler for a single ssh-tui session 2 3 const std = @import("std"); 4 const xit = @import("xit"); 5 const rp = xit.repo; 6 const xitui = xit.xitui; 7 const StreamTerminal = xitui.stream_terminal.StreamTerminal; 8 const Grid = xitui.grid.Grid; 9 const Size = xitui.layout.Size; 10 const ui = @import("./ui.zig"); 11 const ssh_tui = @import("./ssh_tui.zig"); 12 13 pub fn handleConnection( 14 io: std.Io, 15 allocator: std.mem.Allocator, 16 stream: std.Io.net.Stream, 17 admin_repo_path: []const u8, 18 ) !void { 19 var recv_buf: [4096]u8 = undefined; 20 var send_buf: [4096]u8 = undefined; 21 var stream_reader = stream.reader(io, &recv_buf); 22 var stream_writer = stream.writer(io, &send_buf); 23 const reader = &stream_reader.interface; 24 const writer = &stream_writer.interface; 25 26 // read the prelude. if it fails we owe the client a plain-text reason 27 // before we drop the connection — the helper is going to dump whatever 28 // we send to the user's terminal, and a silent close looks like a bug. 29 const prelude = ssh_tui.readPrelude(allocator, reader) catch |err| { 30 writer.print("haxy ssh-tui: invalid prelude: {s}\n", .{@errorName(err)}) catch {}; 31 writer.flush() catch {}; 32 return err; 33 }; 34 defer prelude.deinit(allocator); 35 36 var page_arena = std.heap.ArenaAllocator.init(allocator); 37 defer page_arena.deinit(); 38 39 const repo_opts: rp.RepoOpts(.xit) = .{}; 40 const Repo = rp.Repo(.xit, repo_opts); 41 var repo_or_err = Repo.open(io, allocator, .{ .path = admin_repo_path }); 42 const page: ui.Page = if (repo_or_err) |*repo| blk: { 43 defer repo.deinit(io, allocator); 44 break :blk .{ .users_and_repos = try .init(repo_opts, &page_arena, repo) }; 45 } else |_| .{ .users_and_repos = .empty() }; 46 47 var root = try ui.initRoot(allocator, &page); 48 defer root.deinit(); 49 50 var terminal = try StreamTerminal.init(allocator, writer, .{ 51 .width = prelude.width, 52 .height = prelude.height, 53 }); 54 defer terminal.deinit(); 55 56 // initial render so the user sees something before they type 57 var last_size = Size{ .width = 0, .height = 0 }; 58 var last_grid = try Grid.init(allocator, last_size); 59 defer last_grid.deinit(); 60 _ = try terminal.render(&root, &last_grid, &last_size); 61 try root.build(.{ 62 .min_size = .{ .width = null, .height = null }, 63 .max_size = .{ .width = last_size.width, .height = last_size.height }, 64 }, root.getFocus()); 65 66 // frame-decode loop. each iteration reads one frame, dispatches it, and 67 // re-renders. blocking happens naturally in readFrame waiting on the 68 // socket. 69 while (!terminal.shouldQuit()) { 70 const frame = ssh_tui.readFrame(allocator, reader) catch |err| switch (err) { 71 error.EndOfStream => return, 72 else => return err, 73 }; 74 defer frame.deinit(allocator); 75 76 switch (frame) { 77 .data => |payload| { 78 try terminal.writeBytes(payload); 79 while (terminal.popKey()) |key| { 80 try ui.inputKey(&root, key, &terminal); 81 } 82 }, 83 .resize => |sz| { 84 terminal.pushResize(.{ .width = sz.width, .height = sz.height }); 85 while (terminal.popKey()) |key| { 86 try ui.inputKey(&root, key, &terminal); 87 } 88 }, 89 .close => terminal.requestQuit(), 90 } 91 92 try root.build(.{ 93 .min_size = .{ .width = null, .height = null }, 94 .max_size = .{ .width = last_size.width, .height = last_size.height }, 95 }, root.getFocus()); 96 _ = try terminal.render(&root, &last_grid, &last_size); 97 } 98 } 99