|
@@ -36,12 +36,14 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
const theme = useTheme()
|
|
const theme = useTheme()
|
|
|
let container!: HTMLDivElement
|
|
let container!: HTMLDivElement
|
|
|
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
|
|
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
|
|
|
- let ws: WebSocket
|
|
|
|
|
- let term: Term
|
|
|
|
|
|
|
+ let ws: WebSocket | undefined
|
|
|
|
|
+ let term: Term | undefined
|
|
|
let ghostty: Ghostty
|
|
let ghostty: Ghostty
|
|
|
let serializeAddon: SerializeAddon
|
|
let serializeAddon: SerializeAddon
|
|
|
let fitAddon: FitAddon
|
|
let fitAddon: FitAddon
|
|
|
let handleResize: () => void
|
|
let handleResize: () => void
|
|
|
|
|
+ let reconnect: number | undefined
|
|
|
|
|
+ let disposed = false
|
|
|
|
|
|
|
|
const getTerminalColors = (): TerminalColors => {
|
|
const getTerminalColors = (): TerminalColors => {
|
|
|
const mode = theme.mode()
|
|
const mode = theme.mode()
|
|
@@ -76,22 +78,24 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
if (!term || !term.hasSelection()) return false
|
|
if (!term || !term.hasSelection()) return false
|
|
|
const selection = term.getSelection()
|
|
const selection = term.getSelection()
|
|
|
if (!selection) return false
|
|
if (!selection) return false
|
|
|
|
|
+ if (document.body) {
|
|
|
|
|
+ const textarea = document.createElement("textarea")
|
|
|
|
|
+ textarea.value = selection
|
|
|
|
|
+ textarea.setAttribute("readonly", "")
|
|
|
|
|
+ textarea.style.position = "fixed"
|
|
|
|
|
+ textarea.style.opacity = "0"
|
|
|
|
|
+ document.body.appendChild(textarea)
|
|
|
|
|
+ textarea.select()
|
|
|
|
|
+ const copied = document.execCommand("copy")
|
|
|
|
|
+ document.body.removeChild(textarea)
|
|
|
|
|
+ if (copied) return true
|
|
|
|
|
+ }
|
|
|
const clipboard = navigator.clipboard
|
|
const clipboard = navigator.clipboard
|
|
|
if (clipboard?.writeText) {
|
|
if (clipboard?.writeText) {
|
|
|
clipboard.writeText(selection).catch(() => {})
|
|
clipboard.writeText(selection).catch(() => {})
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
- if (!document.body) return false
|
|
|
|
|
- const textarea = document.createElement("textarea")
|
|
|
|
|
- textarea.value = selection
|
|
|
|
|
- textarea.setAttribute("readonly", "")
|
|
|
|
|
- textarea.style.position = "fixed"
|
|
|
|
|
- textarea.style.opacity = "0"
|
|
|
|
|
- document.body.appendChild(textarea)
|
|
|
|
|
- textarea.select()
|
|
|
|
|
- const copied = document.execCommand("copy")
|
|
|
|
|
- document.body.removeChild(textarea)
|
|
|
|
|
- return copied
|
|
|
|
|
|
|
+ return false
|
|
|
}
|
|
}
|
|
|
const handlePointerDown = () => {
|
|
const handlePointerDown = () => {
|
|
|
const activeElement = document.activeElement
|
|
const activeElement = document.activeElement
|
|
@@ -104,8 +108,12 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
onMount(async () => {
|
|
onMount(async () => {
|
|
|
ghostty = await Ghostty.load()
|
|
ghostty = await Ghostty.load()
|
|
|
|
|
|
|
|
- ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
|
|
|
|
|
- term = new Term({
|
|
|
|
|
|
|
+ const socket = new WebSocket(
|
|
|
|
|
+ sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`,
|
|
|
|
|
+ )
|
|
|
|
|
+ ws = socket
|
|
|
|
|
+
|
|
|
|
|
+ const t = new Term({
|
|
|
cursorBlink: true,
|
|
cursorBlink: true,
|
|
|
fontSize: 14,
|
|
fontSize: 14,
|
|
|
fontFamily: "IBM Plex Mono, monospace",
|
|
fontFamily: "IBM Plex Mono, monospace",
|
|
@@ -114,7 +122,9 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
scrollback: 10_000,
|
|
scrollback: 10_000,
|
|
|
ghostty,
|
|
ghostty,
|
|
|
})
|
|
})
|
|
|
- term.attachCustomKeyEventHandler((event) => {
|
|
|
|
|
|
|
+ term = t
|
|
|
|
|
+
|
|
|
|
|
+ t.attachCustomKeyEventHandler((event) => {
|
|
|
const key = event.key.toLowerCase()
|
|
const key = event.key.toLowerCase()
|
|
|
if (key === "c") {
|
|
if (key === "c") {
|
|
|
const macCopy = event.metaKey && !event.ctrlKey && !event.altKey
|
|
const macCopy = event.metaKey && !event.ctrlKey && !event.altKey
|
|
@@ -134,21 +144,21 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
|
|
|
|
|
fitAddon = new FitAddon()
|
|
fitAddon = new FitAddon()
|
|
|
serializeAddon = new SerializeAddon()
|
|
serializeAddon = new SerializeAddon()
|
|
|
- term.loadAddon(serializeAddon)
|
|
|
|
|
- term.loadAddon(fitAddon)
|
|
|
|
|
|
|
+ t.loadAddon(serializeAddon)
|
|
|
|
|
+ t.loadAddon(fitAddon)
|
|
|
|
|
|
|
|
- term.open(container)
|
|
|
|
|
|
|
+ t.open(container)
|
|
|
container.addEventListener("pointerdown", handlePointerDown)
|
|
container.addEventListener("pointerdown", handlePointerDown)
|
|
|
focusTerminal()
|
|
focusTerminal()
|
|
|
|
|
|
|
|
if (local.pty.buffer) {
|
|
if (local.pty.buffer) {
|
|
|
if (local.pty.rows && local.pty.cols) {
|
|
if (local.pty.rows && local.pty.cols) {
|
|
|
- term.resize(local.pty.cols, local.pty.rows)
|
|
|
|
|
|
|
+ t.resize(local.pty.cols, local.pty.rows)
|
|
|
}
|
|
}
|
|
|
- term.reset()
|
|
|
|
|
- term.write(local.pty.buffer, () => {
|
|
|
|
|
|
|
+ t.reset()
|
|
|
|
|
+ t.write(local.pty.buffer, () => {
|
|
|
if (local.pty.scrollY) {
|
|
if (local.pty.scrollY) {
|
|
|
- term.scrollToLine(local.pty.scrollY)
|
|
|
|
|
|
|
+ t.scrollToLine(local.pty.scrollY)
|
|
|
}
|
|
}
|
|
|
fitAddon.fit()
|
|
fitAddon.fit()
|
|
|
})
|
|
})
|
|
@@ -157,8 +167,8 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
fitAddon.observeResize()
|
|
fitAddon.observeResize()
|
|
|
handleResize = () => fitAddon.fit()
|
|
handleResize = () => fitAddon.fit()
|
|
|
window.addEventListener("resize", handleResize)
|
|
window.addEventListener("resize", handleResize)
|
|
|
- term.onResize(async (size) => {
|
|
|
|
|
- if (ws && ws.readyState === WebSocket.OPEN) {
|
|
|
|
|
|
|
+ t.onResize(async (size) => {
|
|
|
|
|
+ if (socket.readyState === WebSocket.OPEN) {
|
|
|
await sdk.client.pty
|
|
await sdk.client.pty
|
|
|
.update({
|
|
.update({
|
|
|
ptyID: local.pty.id,
|
|
ptyID: local.pty.id,
|
|
@@ -170,39 +180,39 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
.catch(() => {})
|
|
.catch(() => {})
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
- term.onData((data) => {
|
|
|
|
|
- if (ws && ws.readyState === WebSocket.OPEN) {
|
|
|
|
|
- ws.send(data)
|
|
|
|
|
|
|
+ t.onData((data) => {
|
|
|
|
|
+ if (socket.readyState === WebSocket.OPEN) {
|
|
|
|
|
+ socket.send(data)
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
- term.onKey((key) => {
|
|
|
|
|
|
|
+ t.onKey((key) => {
|
|
|
if (key.key == "Enter") {
|
|
if (key.key == "Enter") {
|
|
|
props.onSubmit?.()
|
|
props.onSubmit?.()
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
- // term.onScroll((ydisp) => {
|
|
|
|
|
|
|
+ // t.onScroll((ydisp) => {
|
|
|
// console.log("Scroll position:", ydisp)
|
|
// console.log("Scroll position:", ydisp)
|
|
|
// })
|
|
// })
|
|
|
- ws.addEventListener("open", () => {
|
|
|
|
|
|
|
+ socket.addEventListener("open", () => {
|
|
|
console.log("WebSocket connected")
|
|
console.log("WebSocket connected")
|
|
|
sdk.client.pty
|
|
sdk.client.pty
|
|
|
.update({
|
|
.update({
|
|
|
ptyID: local.pty.id,
|
|
ptyID: local.pty.id,
|
|
|
size: {
|
|
size: {
|
|
|
- cols: term.cols,
|
|
|
|
|
- rows: term.rows,
|
|
|
|
|
|
|
+ cols: t.cols,
|
|
|
|
|
+ rows: t.rows,
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
|
.catch(() => {})
|
|
.catch(() => {})
|
|
|
})
|
|
})
|
|
|
- ws.addEventListener("message", (event) => {
|
|
|
|
|
- term.write(event.data)
|
|
|
|
|
|
|
+ socket.addEventListener("message", (event) => {
|
|
|
|
|
+ t.write(event.data)
|
|
|
})
|
|
})
|
|
|
- ws.addEventListener("error", (error) => {
|
|
|
|
|
|
|
+ socket.addEventListener("error", (error) => {
|
|
|
console.error("WebSocket error:", error)
|
|
console.error("WebSocket error:", error)
|
|
|
props.onConnectError?.(error)
|
|
props.onConnectError?.(error)
|
|
|
})
|
|
})
|
|
|
- ws.addEventListener("close", () => {
|
|
|
|
|
|
|
+ socket.addEventListener("close", () => {
|
|
|
console.log("WebSocket disconnected")
|
|
console.log("WebSocket disconnected")
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
@@ -212,18 +222,21 @@ export const Terminal = (props: TerminalProps) => {
|
|
|
window.removeEventListener("resize", handleResize)
|
|
window.removeEventListener("resize", handleResize)
|
|
|
}
|
|
}
|
|
|
container.removeEventListener("pointerdown", handlePointerDown)
|
|
container.removeEventListener("pointerdown", handlePointerDown)
|
|
|
- if (serializeAddon && props.onCleanup) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const t = term
|
|
|
|
|
+ if (serializeAddon && props.onCleanup && t) {
|
|
|
const buffer = serializeAddon.serialize()
|
|
const buffer = serializeAddon.serialize()
|
|
|
props.onCleanup({
|
|
props.onCleanup({
|
|
|
...local.pty,
|
|
...local.pty,
|
|
|
buffer,
|
|
buffer,
|
|
|
- rows: term.rows,
|
|
|
|
|
- cols: term.cols,
|
|
|
|
|
- scrollY: term.getViewportY(),
|
|
|
|
|
|
|
+ rows: t.rows,
|
|
|
|
|
+ cols: t.cols,
|
|
|
|
|
+ scrollY: t.getViewportY(),
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
ws?.close()
|
|
ws?.close()
|
|
|
- term?.dispose()
|
|
|
|
|
|
|
+ t?.dispose()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
return (
|
|
return (
|