╭ changed ────╮ │build.zig.zon│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 3 .version = "0.0.0", │ │ 4 .dependencies = .{ │ │ 5 .xit = .{ │ │ 6 - .url = "git+https://github.com/xit-vcs/xit#79042ff35368db1b5eb0cc187d34b2c4ccd70603",│ │ 7 - .hash = "xit-0.0.0-Ub3nDDKUTwHvU_taqYYqUs7Pbx8kg5Yrgcks_URi4AKm", │ │ 6 + .url = "git+https://github.com/xit-vcs/xit#bcc9ccf21efc8d62c1bbcf8a37091723e4f50884",│ │ 7 + .hash = "xit-0.0.0-Ub3nDDKUTwEKT5xqOaq0yhXX2REtg50tPH2AXkj1G0cG", │ │ 8 }, │ │ 9 }, │ │10 .paths = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/command.zig│ ╰───────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │23 \\a long-running server forwarding receive-pack and upload-pack. │ │24 , │ │25 .example = │ │26 - \\haxy serve --http-listen 127.0.0.1:8080 --ssh-listen 127.0.0.1:8081 --wui-listen 127.0.0.1:8082 --data-dir /srv/git │ │26 + \\haxy serve --http-listen 127.0.0.1:8080 --ssh-listen 127.0.0.1:8081 --tui-listen 127.0.0.1:8082 --wui-listen 127.0.0.1:8000 --data-dir /srv/git│ │27 , │ │28 }, │ │29 .ssh_git => .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────╮ │src/serve.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────╮ │ 4 const hash = xit.hash; │ │ 5 const ui = @import("./ui.zig"); │ │ 6 const web = @import("./web.zig"); │ │ 7 +const serve_ssh_tui = @import("./serve_ssh_tui.zig");│ │ 8 │ │ 9 pub const Options = struct { │ │10 http_listen: []const u8 = "127.0.0.1:8080", │ ╰─────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │63 const ssh_address = try std.Io.net.IpAddress.parseIp4(ssh_listen_address.host, ssh_listen_address.port);│ │64 var ssh_server = try ssh_address.listen(io, .{ .reuse_address = true }); │ │65 defer ssh_server.deinit(io); │ │66 + │ │67 + // create tui listener │ │68 + │ │69 + const tui_listen_address = try parseListenAddress(options.tui_listen); │ │70 + const tui_address = try std.Io.net.IpAddress.parseIp4(tui_listen_address.host, tui_listen_address.port);│ │71 + var tui_server = try tui_address.listen(io, .{ .reuse_address = true }); │ │72 + defer tui_server.deinit(io); │ │73 │ │74 // create wui listener │ │75 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 95 │ │ 96 runSshListener(repo_kind, any_repo_opts, io, allocator, repo_root_path, &ssh_server, &tasks, err);│ │ 97 │ │ 98 + try err.print("serving TUI sessions on {s}\n", .{options.tui_listen}); │ │ 99 + try err.flush(); │ │100 + │ │101 + runTuiListener(io, allocator, admin_repo_path, &tui_server, &tasks, err); │ │102 + │ │103 try err.print("serving web UI on http://{s}/\n", .{options.wui_listen}); │ │104 try err.flush(); │ │105 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │369 }}); │ │370 } │ │371 │ │372 +fn runTuiListener( │ │373 + io: std.Io, │ │374 + allocator: std.mem.Allocator, │ │375 + admin_repo_path: []const u8, │ │376 + net_server: *std.Io.net.Server, │ │377 + tasks: *std.Io.Group, │ │378 + err: *std.Io.Writer, │ │379 +) void { │ │380 + const Listener = struct { │ │381 + io: std.Io, │ │382 + allocator: std.mem.Allocator, │ │383 + admin_repo_path: []const u8, │ │384 + net_server: *std.Io.net.Server, │ │385 + tasks: *std.Io.Group, │ │386 + err: *std.Io.Writer, │ │387 + │ │388 + fn run(ctx: @This()) void { │ │389 + while (true) { │ │390 + const stream = ctx.net_server.accept(ctx.io) catch |accept_err| { │ │391 + if (accept_err == error.Canceled) return; │ │392 + logError(ctx.err, "tui accept failed: {s}\n", .{@errorName(accept_err)}); │ │393 + continue; │ │394 + }; │ │395 + │ │396 + const Connection = struct { │ │397 + io: std.Io, │ │398 + allocator: std.mem.Allocator, │ │399 + admin_repo_path: []const u8, │ │400 + stream: std.Io.net.Stream, │ │401 + err: *std.Io.Writer, │ │402 + │ │403 + fn run(conn: @This()) void { │ │404 + defer conn.stream.close(conn.io); │ │405 + serve_ssh_tui.handleConnection(conn.io, conn.allocator, conn.stream, conn.admin_repo_path) catch |session_err| {│ │406 + logError(conn.err, "tui session failed: {s}\n", .{@errorName(session_err)}); │ │407 + }; │ │408 + } │ │409 + }; │ │410 + │ │411 + ctx.tasks.async(ctx.io, Connection.run, .{Connection{ │ │412 + .io = ctx.io, │ │413 + .allocator = ctx.allocator, │ │414 + .admin_repo_path = ctx.admin_repo_path, │ │415 + .stream = stream, │ │416 + .err = ctx.err, │ │417 + }}); │ │418 + } │ │419 + } │ │420 + }; │ │421 + │ │422 + tasks.async(io, Listener.run, .{Listener{ │ │423 + .io = io, │ │424 + .allocator = allocator, │ │425 + .admin_repo_path = admin_repo_path, │ │426 + .net_server = net_server, │ │427 + .tasks = tasks, │ │428 + .err = err, │ │429 + }}); │ │430 +} │ │431 + │ │432 fn handleSshGitConnection( │ │433 comptime repo_kind: rp.RepoKind, │ │434 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │26 var last_grid = try Grid.init(allocator, last_size); │ │27 defer last_grid.deinit(); │ │28 │ │29 - while (!term.quit.load(.monotonic)) { │ │29 + while (!terminal.shouldQuit()) { │ │30 const grid_changed = try terminal.render(&root, &last_grid, &last_size);│ │31 │ │32 // process any inputs. │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │41 // there may be an animation that requires more looping. │ │42 var blocking = !grid_changed; │ │43 while (try terminal.readKey(io, blocking)) |key| { │ │44 - switch (key) { │ │45 - .codepoint => |cp| if (cp == 'q') return else try root.input(key, root.getFocus()),│ │46 - .mouse => |mouse| { │ │47 - if (mouse.action == .press and mouse.action.press == .left) { │ │48 - const root_focus = root.getFocus(); │ │49 - var iter = root_focus.children.iterator(); │ │50 - while (iter.next()) |entry| { │ │51 - const child = entry.value_ptr.*; │ │52 - if (!child.focus.focusable) continue; │ │53 - const r = child.rect; │ │54 - if (mouse.x >= r.x and mouse.y >= r.y and │ │55 - mouse.x < r.x + r.size.width and mouse.y < r.y + r.size.height) │ │56 - { │ │57 - try root_focus.setFocus(entry.key_ptr.*); │ │58 - break; │ │59 - } │ │60 - } │ │61 - } else { │ │62 - try root.input(key, root.getFocus()); │ │63 - } │ │64 - }, │ │65 - else => try root.input(key, root.getFocus()), │ │66 - } │ │44 + try inputKey(&root, key, &terminal); │ │45 blocking = false; │ │46 } │ │47 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │52 } │ │53 } │ │54 │ │55 +// route a single decoded key into the widget tree, handling 'q'-to-quit and │ │56 +// mouse-click focus traversal along the way. shared between the tty event │ │57 +// loop (ui.run) and the SSH session event loop in serve_ssh_tui.zig. │ │58 +// `terminal` is whatever terminal type the caller is driving — it must │ │59 +// expose requestQuit(). │ │60 +pub fn inputKey(root: *Widget, key: inp.Key, terminal: anytype) !void { │ │61 + switch (key) { │ │62 + .codepoint => |cp| if (cp == 'q') terminal.requestQuit() else try root.input(key, root.getFocus()),│ │63 + .mouse => |mouse| { │ │64 + if (mouse.action == .press and mouse.action.press == .left) { │ │65 + const root_focus = root.getFocus(); │ │66 + var iter = root_focus.children.iterator(); │ │67 + while (iter.next()) |entry| { │ │68 + const child = entry.value_ptr.*; │ │69 + if (!child.focus.focusable) continue; │ │70 + const r = child.rect; │ │71 + if (mouse.x >= r.x and mouse.y >= r.y and │ │72 + mouse.x < r.x + r.size.width and mouse.y < r.y + r.size.height) │ │73 + { │ │74 + try root_focus.setFocus(entry.key_ptr.*); │ │75 + break; │ │76 + } │ │77 + } │ │78 + } else { │ │79 + try root.input(key, root.getFocus()); │ │80 + } │ │81 + }, │ │82 + else => try root.input(key, root.getFocus()), │ │83 + } │ │84 +} │ │85 + │ │86 pub fn initRoot(allocator: std.mem.Allocator, page: *const Page) !Widget { │ │87 var root: Widget = switch (page.*) { │ │88 .users_and_repos => |*p| .{ .users_and_repos = try UsersAndRepos.View.init(allocator, p) }, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ added ──────────────╮ │src/serve_ssh_tui.zig│ ╰─────────────────────╯