update working ssh shell

This commit is contained in:
2024-11-06 06:24:14 +00:00
parent acd843d31c
commit 86eb5eb4e6
12 changed files with 1789 additions and 2183 deletions
+139
View File
@@ -0,0 +1,139 @@
import React, { ComponentPropsWithoutRef } from "react";
import XTermJs, { XTermRef } from "./xtermjs";
import Ionicons from "@expo/vector-icons/Ionicons";
import {
Pressable,
ScrollView,
StyleProp,
StyleSheet,
Text,
TextStyle,
View,
} from "react-native";
const Keys = {
ArrowLeft: "\x1b[D",
ArrowRight: "\x1b[C",
ArrowUp: "\x1b[A",
ArrowDown: "\x1b[B",
Enter: "\x0D",
Escape: "\x1b",
Home: "\x1b[H",
End: "\x1b[F",
PageUp: "\x1b[5~",
PageDown: "\x1b[6~",
Alt: "\x1b",
Tab: "\x09",
};
type TerminalProps = ComponentPropsWithoutRef<typeof View> & {
wsUrl: string;
};
const Terminal = ({ wsUrl, style, ...props }: TerminalProps) => {
const ref = React.useRef<XTermRef>(null);
const send = (data: string) => {
ref.current?.send(data);
};
return (
<View style={[styles.container, style]} {...props}>
<XTermJs ref={ref} dom={{ scrollEnabled: false }} wsUrl={wsUrl} />
<ScrollView
horizontal
style={{ flexGrow: 0 }}
contentContainerStyle={styles.buttons}
>
<TerminalButton
title={<Ionicons name="swap-horizontal" color="white" size={16} />}
onPress={() => send(Keys.Tab)}
/>
<TerminalButton title="ESC" onPress={() => send(Keys.Escape)} />
<TerminalButton
title={<Ionicons name="home" color="white" size={16} />}
onPress={() => send(Keys.Home)}
/>
<TerminalButton
title={<Ionicons name="arrow-back" color="white" size={18} />}
onPress={() => send(Keys.ArrowLeft)}
/>
<TerminalButton
title={<Ionicons name="arrow-up" color="white" size={18} />}
onPress={() => send(Keys.ArrowUp)}
/>
<TerminalButton
title={<Ionicons name="arrow-down" color="white" size={18} />}
onPress={() => send(Keys.ArrowDown)}
/>
<TerminalButton
title={<Ionicons name="arrow-forward" color="white" size={18} />}
onPress={() => send(Keys.ArrowRight)}
/>
<TerminalButton title="Enter" onPress={() => send(Keys.Enter)} />
<TerminalButton title="End" onPress={() => send(Keys.End)} />
<TerminalButton title="PgUp" onPress={() => send(Keys.PageUp)} />
<TerminalButton title="PgDn" onPress={() => send(Keys.PageDown)} />
{/* <TerminalButton title="Alt" onPress={() => send(Keys.Alt)} /> */}
<TerminalButton title="^C" onPress={() => send("\x03")} />
<TerminalButton title="^D" onPress={() => send("\x04")} />
<TerminalButton title="^Q" onPress={() => send("\x11")} />
<TerminalButton title="^V" onPress={() => send("\x11")} />
<TerminalButton title="^S" onPress={() => send("\x13")} />
<TerminalButton title="^W" onPress={() => send("\x18")} />
<TerminalButton title="^X" onPress={() => send("\x18")} />
<TerminalButton title="^Z" onPress={() => send("\x1a")} />
</ScrollView>
</View>
);
};
const TerminalButton = ({
title,
textStyle,
...props
}: ComponentPropsWithoutRef<typeof Pressable> & {
title: string | React.ReactNode;
textStyle?: StyleProp<TextStyle>;
}) => (
<Pressable
style={({ pressed }) => [styles.btn, pressed && styles.btnPressed]}
{...props}
>
{typeof title === "string" ? (
<Text style={[styles.btnText, textStyle]}>{title}</Text>
) : (
title
)}
</Pressable>
);
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#232323",
},
buttons: {
display: "flex",
flexDirection: "row",
alignItems: "stretch",
backgroundColor: "#232323",
},
btn: {
display: "flex",
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 14,
paddingVertical: 10,
},
btnPressed: {
backgroundColor: "#3a3a3a",
},
btnText: {
color: "white",
fontSize: 16,
},
});
export default Terminal;
+130
View File
@@ -0,0 +1,130 @@
"use dom";
import React, { CSSProperties, FC, forwardRef, useEffect, useRef } from "react";
import {
DOMImperativeFactory,
DOMProps,
useDOMImperativeHandle,
} from "expo/dom";
import { Terminal as XTerm } from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css";
import { FitAddon } from "@xterm/addon-fit/src/FitAddon";
import { AttachAddon } from "@xterm/addon-attach/src/AttachAddon";
import { JSONValue } from "expo/build/dom/dom.types";
type XTermJsProps = {
onLoad?: () => void;
dom?: DOMProps;
style?: CSSProperties;
wsUrl: string;
};
// @ts-ignore
const IS_DOM = typeof ReactNativeWebView !== "undefined";
export interface XTermRef extends DOMImperativeFactory {
send: (...args: JSONValue[]) => void;
}
const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const { wsUrl, onLoad, style = {} } = props;
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
const xterm = new XTerm();
xterm.open(container);
const fitAddon = new FitAddon();
xterm.loadAddon(fitAddon);
fitAddon.fit();
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
const attachAddon = new AttachAddon(ws);
xterm.loadAddon(attachAddon);
if (xterm.element) {
xterm.element.style.height = "100%";
}
if (IS_DOM) {
xterm.focus();
}
function resizeTerminal() {
const { cols, rows } = xterm;
ws.send(`\x01${cols},${rows}`);
}
function onResize() {
fitAddon.fit();
resizeTerminal();
}
function onOpen() {
resizeTerminal();
}
function onClose(e: CloseEvent) {
// Check if the close event was abnormal
if (!e.wasClean) {
const reason = e.reason || `Code: ${e.code}`;
xterm.write(`\r\nConnection closed unexpectedly: ${reason}\r\n`);
} else {
xterm.write("\r\nConnection closed.\r\n");
}
}
ws.addEventListener("open", onOpen);
ws.addEventListener("close", onClose);
xterm.onResize(resizeTerminal);
window.addEventListener("resize", onResize);
onLoad?.();
return () => {
xterm.dispose();
wsRef.current = null;
containerRef.current = null;
ws.close();
ws.removeEventListener("open", onOpen);
ws.removeEventListener("close", onClose);
window.removeEventListener("resize", onResize);
};
}, [wsUrl]);
useDOMImperativeHandle(
ref,
() => ({
send: (...args) => {
const ws = wsRef.current;
if (ws?.readyState === ws?.OPEN && args[0]) {
ws?.send(String(args[0]));
}
},
}),
[]
);
return (
<div
ref={containerRef}
style={{
flex: !IS_DOM ? 1 : undefined,
width: "100%",
height: IS_DOM ? "100vh" : undefined,
overflow: "hidden",
...style,
}}
/>
);
});
export default XTermJs;