╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ ssh_tui.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
167 168 pub const Options = struct { 169 tui_connect: []const u8 = blk: { 170 const srv = @import("./serve.zig"); 171 const opts: srv.Options = .{}; 172 break :blk opts.tui_listen; 173 }, 174 user_key: ?[]const u8 = null, 175 }; 176 177 // SIGWINCH delivers no context; the handler reads this file-scope fd to 178 // know where to write its wake-up byte. set by run() before sigaction is 179 // installed; reset on the way out. 180 var sigwinch_pipe_write: std.posix.fd_t = -1; 181 182 pub fn run( 183 io: std.Io, 184 allocator: std.mem.Allocator, 185 options: Options, 186 environ_map: *std.process.Environ.Map, 187 ) !void { 188 _ = allocator; 189 190 if (builtin.os.tag == .windows) return error.WindowsNotSupported; 191 192 const user_key = options.user_key orelse return error.MissingUserKey; 193 const term_name = environ_map.get("TERM") orelse "xterm"; 194 195 const initial_size = try getWinSize(io); 196 197 const conn = try ssh.parseConnectAddress(options.tui_connect); 198 const address = try std.Io.net.IpAddress.parseIp4(conn.host, conn.port); 199 const stream = try address.connect(io, .{ .mode = .stream }); 200 defer stream.close(io); 201 202 // put stdin into raw mode so the server sees user keystrokes as-is. 203 // restore the original termios on the way out so we don't leave the 204 // user's shell in a broken state if anything throws. 205 const cooked = try std.posix.tcgetattr(std.posix.STDIN_FILENO); 206 defer std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, cooked) catch {}; 207 try setRawMode(std.posix.STDIN_FILENO, cooked); 208 209 // self-pipe: SIGWINCH handler writes a byte, stdinProxy polls on the 210 // read end and converts it into a resize frame. 211 const pipe_fds = try std.Io.Threaded.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); 212 defer { 213 sigwinch_pipe_write = -1; 214 _ = std.posix.system.close(pipe_fds[1]); 215 _ = std.posix.system.close(pipe_fds[0]); 216 } 217 sigwinch_pipe_write = pipe_fds[1]; 218 219 std.posix.sigaction(std.posix.SIG.WINCH, &.{ 220 .handler = .{ .handler = sigwinchHandler }, 221 .mask = std.posix.sigemptyset(), 222 .flags = 0, 223 }, null); 224 225 // prelude 226 var prelude_buf: [1024]u8 = undefined; 227 var prelude_writer = stream.writer(io, &prelude_buf); 228 try writePrelude(&prelude_writer.interface, .{ 229 .user_key = user_key, 230 .term = term_name, 231 .width = initial_size.width, 232 .height = initial_size.height, 233 }); 234 try prelude_writer.interface.flush(); 235 236 // stdin proxy thread: frames stdin bytes + emits resize frames on 237 // SIGWINCH. detached so we don't have to coordinate shutdown — when 238 // run() returns, the process exits and the thread goes with it. 239 const ctx = StdinThreadCtx{ .io = io, .stream = stream, .pipe_read = pipe_fds[0] }; 240 const stdin_thread = try std.Thread.spawn(.{}, stdinProxy, .{ctx}); 241 stdin_thread.detach(); 242 243 // main: socket → stdout, raw. server's already-formed ANSI bytes pass 244 // straight through to the user's terminal. 245 try ssh.copyFd(stream.socket.handle, std.posix.STDOUT_FILENO); 246 } 247 248 fn sigwinchHandler(_: std.posix.SIG) callconv(.c) void { 249 const fd = sigwinch_pipe_write; 250 if (fd >= 0) { 251 const byte = [_]u8{1}; 252 _ = std.posix.system.write(fd, byte[0..].ptr, 1); 253 } 254 } 255 256 fn setRawMode(fd: std.posix.fd_t, cooked: std.posix.termios) !void { 257 var raw = cooked; 258 raw.iflag = .{}; 259 raw.oflag = .{}; 260 raw.lflag = .{}; 261 raw.cflag.CSIZE = .CS8; 262 raw.cc[@intFromEnum(std.posix.V.MIN)] = 1; 263 raw.cc[@intFromEnum(std.posix.V.TIME)] = 0; 264 try std.posix.tcsetattr(fd, .FLUSH, raw); 265 } 266 267 const WinSize = struct { width: u16, height: u16 }; 268 269 // fall back to a conventional 80x24 if the PTY hasn't been told a size yet — 270 // this happens when the SSH client has no local controlling terminal (e.g. 271 // non-interactive piped input) and never sends a window-change request. 272 const default_width: u16 = 80; 273 const default_height: u16 = 24; 274 275 fn getWinSize(io: std.Io) !WinSize { 276 var ws: std.posix.winsize = undefined; 277 const result = (try io.operate(.{ .device_io_control = .{ 278 .file = std.Io.File.stdout(), 279 .code = std.posix.T.IOCGWINSZ, 280 .arg = &ws, 281 } })).device_io_control; 282 if (result < 0) return error.IoctlFailed; 283 return .{ 284 .width = if (ws.col == 0) default_width else ws.col, 285 .height = if (ws.row == 0) default_height else ws.row, 286 }; 287 } 288 289 const StdinThreadCtx = struct { 290 io: std.Io, 291 stream: std.Io.net.Stream, 292 pipe_read: std.posix.fd_t, 293 }; 294 295 fn stdinProxy(ctx: StdinThreadCtx) void { 296 var send_buf: [4096]u8 = undefined; 297 var stream_writer = ctx.stream.writer(ctx.io, &send_buf); 298 const w = &stream_writer.interface; 299 300 while (true) { 301 var fds = [_]std.posix.pollfd{ 302 .{ .fd = std.posix.STDIN_FILENO, .events = std.posix.POLL.IN, .revents = 0 }, 303 .{ .fd = ctx.pipe_read, .events = std.posix.POLL.IN, .revents = 0 }, 304 }; 305 _ = std.posix.poll(&fds, -1) catch return; 306 307 if (fds[1].revents & std.posix.POLL.IN != 0) { 308 // drain whatever's in the pipe — multiple coalesced SIGWINCHes 309 // all collapse into one resize-frame emission. 310 var drain: [16]u8 = undefined; 311 _ = std.posix.read(ctx.pipe_read, &drain) catch {}; 312 if (getWinSize(ctx.io)) |size| { 313 writeResizeFrame(w, size.width, size.height) catch return; 314 w.flush() catch return; 315 } else |_| {} 316 } 317 if (fds[0].revents & std.posix.POLL.IN != 0) { 318 var buf: [4096]u8 = undefined; 319 const n = std.posix.read(std.posix.STDIN_FILENO, &buf) catch return; 320 if (n == 0) { 321 writeCloseFrame(w) catch {}; 322 w.flush() catch {}; 323 ctx.stream.shutdown(ctx.io, .send) catch {}; 324 return; 325 } 326 writeDataFrame(w, buf[0..n]) catch return; 327 w.flush() catch return; 328 } 329 } 330 } 331