diff --git a/cmd/tsconnect/package.json b/cmd/tsconnect/package.json index 28ebd6762..fabf9910a 100644 --- a/cmd/tsconnect/package.json +++ b/cmd/tsconnect/package.json @@ -10,7 +10,7 @@ "qrcode": "^1.5.0", "tailwindcss": "^3.1.6", "typescript": "^4.7.4", - "xterm": "^4.18.0", + "xterm": "mihaip/xterm.js#95100e3c870b59348bb4fa5504bab9f9317bac67", "xterm-addon-fit": "^0.5.0" }, "scripts": { diff --git a/cmd/tsconnect/src/app/ssh.tsx b/cmd/tsconnect/src/app/ssh.tsx index 36c85dddc..bd8ee4690 100644 --- a/cmd/tsconnect/src/app/ssh.tsx +++ b/cmd/tsconnect/src/app/ssh.tsx @@ -2,16 +2,24 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -import { useState, useCallback } from "preact/hooks" +import { useState, useCallback, useMemo, useEffect, useRef } from "preact/hooks" +import { createPortal } from "preact/compat" +import type { VNode } from "preact" import { runSSHSession, SSHSessionDef } from "../lib/ssh" export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) { - const [sshSessionDef, setSSHSessionDef] = useState(null) + const [sshSessionDef, setSSHSessionDef] = useState( + null + ) const clearSSHSessionDef = useCallback(() => setSSHSessionDef(null), []) if (sshSessionDef) { - return ( + const sshSession = ( ) + if (sshSessionDef.newWindow) { + return {sshSession} + } + return sshSession } const sshPeers = netMap.peers.filter( (p) => p.tailscaleSSHEnabled && p.online !== false @@ -24,6 +32,8 @@ export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) { return } +type SSHFormSessionDef = SSHSessionDef & { newWindow?: boolean } + function SSHSession({ def, ipn, @@ -33,20 +43,14 @@ function SSHSession({ ipn: IPN onDone: () => void }) { - return ( -
{ - if (node) { - // Run the SSH session aysnchronously, so that the React render - // loop is complete (otherwise the SSH form may still be visible, - // which affects the size of the terminal, leading to a spurious - // initial resize). - setTimeout(() => runSSHSession(node, def, ipn, onDone), 0) - } - }} - /> - ) + const ref = useRef(null) + useEffect(() => { + if (ref.current) { + runSSHSession(ref.current, def, ipn, onDone) + } + }, [ref]) + + return
} function NoSSHPeers() { @@ -66,7 +70,7 @@ function SSHForm({ onSubmit, }: { sshPeers: IPNNetMapPeerNode[] - onSubmit: (def: SSHSessionDef) => void + onSubmit: (def: SSHFormSessionDef) => void }) { sshPeers = sshPeers.slice().sort((a, b) => a.name.localeCompare(b.name)) const [username, setUsername] = useState("") @@ -99,7 +103,51 @@ function SSHForm({ type="submit" class="button bg-green-500 border-green-500 text-white hover:bg-green-600 hover:border-green-600" value="SSH" + onClick={(e) => { + if (e.altKey) { + e.preventDefault() + e.stopPropagation() + onSubmit({ username, hostname, newWindow: true }) + } + }} /> ) } + +const NewWindow = ({ + children, + close, +}: { + children: VNode + close: () => void +}) => { + const newWindow = useMemo(() => { + const newWindow = window.open(undefined, undefined, "width=600,height=400") + if (newWindow) { + const containerNode = newWindow.document.createElement("div") + containerNode.className = "h-screen flex flex-col overflow-hidden" + newWindow.document.body.appendChild(containerNode) + + for (const linkNode of document.querySelectorAll( + "head link[rel=stylesheet]" + )) { + const newLink = document.createElement("link") + newLink.rel = "stylesheet" + newLink.href = (linkNode as HTMLLinkElement).href + newWindow.document.head.appendChild(newLink) + } + } + return newWindow + }, []) + if (!newWindow) { + console.error("Could not open window") + return null + } + newWindow.onbeforeunload = () => { + close() + } + + useEffect(() => () => newWindow.close(), []) + return createPortal(children, newWindow.document.body.lastChild as Element) +} diff --git a/cmd/tsconnect/src/lib/ssh.ts b/cmd/tsconnect/src/lib/ssh.ts index 0735c860a..bbc26eacb 100644 --- a/cmd/tsconnect/src/lib/ssh.ts +++ b/cmd/tsconnect/src/lib/ssh.ts @@ -54,7 +54,10 @@ export function runSSHSession( }) // Make terminal and SSH session track the size of the containing DOM node. - resizeObserver = new ResizeObserver(() => fitAddon.fit()) + resizeObserver = + new termContainerNode.ownerDocument.defaultView!.ResizeObserver(() => + fitAddon.fit() + ) resizeObserver.observe(termContainerNode) term.onResize(({ rows, cols }) => sshSession.resize(rows, cols)) diff --git a/cmd/tsconnect/yarn.lock b/cmd/tsconnect/yarn.lock index cb311ada9..b8b1e9fd7 100644 --- a/cmd/tsconnect/yarn.lock +++ b/cmd/tsconnect/yarn.lock @@ -644,10 +644,9 @@ xterm-addon-fit@^0.5.0: resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596" integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ== -xterm@^4.18.0: - version "4.18.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0.tgz#a1f6ab2c330c3918fb094ae5f4c2562987398ea1" - integrity sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ== +xterm@mihaip/xterm.js#95100e3c870b59348bb4fa5504bab9f9317bac67: + version "4.19.0" + resolved "https://codeload.github.com/mihaip/xterm.js/tar.gz/95100e3c870b59348bb4fa5504bab9f9317bac67" y18n@^4.0.0: version "4.0.3"