mirror of
https://github.com/EthanShoeDev/fressh.git
synced 2026-01-11 06:12:51 +00:00
Better xtermjs api
This commit is contained in:
@@ -49,7 +49,7 @@
|
||||
"expo-haptics": "~15.0.7",
|
||||
"expo-image": "~3.0.8",
|
||||
"expo-linking": "~8.0.8",
|
||||
"expo-router": "6.0.6",
|
||||
"expo-router": "6.0.7",
|
||||
"expo-secure-store": "~15.0.7",
|
||||
"expo-splash-screen": "~31.0.10",
|
||||
"expo-status-bar": "~3.0.8",
|
||||
@@ -72,28 +72,28 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@epic-web/config": "^1.21.3",
|
||||
"@types/react": "~19.1.12",
|
||||
"cmd-ts": "^0.14.1",
|
||||
"eslint": "^9.35.0",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.5.0",
|
||||
"@eslint-react/eslint-plugin": "^1.53.0",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.86.0",
|
||||
"@types/react": "~19.1.12",
|
||||
"@typescript-eslint/parser": "^8.44.0",
|
||||
"@typescript-eslint/utils": "^8.43.0",
|
||||
"cmd-ts": "^0.14.1",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-config-expo": "~10.0.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"globals": "^16.4.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"typescript-eslint": "^8.44.0",
|
||||
"eslint-config-expo": "~10.0.0",
|
||||
"globals": "^16.4.0",
|
||||
"jiti": "^2.5.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-organize-imports": "^4.2.0",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "~5.9.2"
|
||||
"typescript": "~5.9.2",
|
||||
"typescript-eslint": "^8.44.0"
|
||||
},
|
||||
"expo": {
|
||||
"doctor": {
|
||||
|
||||
@@ -50,6 +50,8 @@ function RouteSkeleton() {
|
||||
);
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
function ShellDetail() {
|
||||
const xtermRef = useRef<XtermWebViewHandle>(null);
|
||||
const terminalReadyRef = useRef(false);
|
||||
@@ -151,100 +153,72 @@ function ShellDetail() {
|
||||
<XtermJsWebView
|
||||
ref={xtermRef}
|
||||
style={{ flex: 1 }}
|
||||
// WebView behavior that suits terminals
|
||||
keyboardDisplayRequiresUserAction={false}
|
||||
setSupportMultipleWindows={false}
|
||||
overScrollMode="never"
|
||||
pullToRefreshEnabled={false}
|
||||
bounces={false}
|
||||
setBuiltInZoomControls={false}
|
||||
setDisplayZoomControls={false}
|
||||
textZoom={100}
|
||||
allowsLinkPreview={false}
|
||||
textInteractionEnabled={false}
|
||||
logger={{
|
||||
log: console.log,
|
||||
debug: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
}}
|
||||
// xterm options
|
||||
options={{
|
||||
fontFamily: 'Menlo, ui-monospace, monospace',
|
||||
fontSize: 80,
|
||||
cursorBlink: true,
|
||||
scrollback: 10000,
|
||||
xtermOptions={{
|
||||
theme: {
|
||||
background: theme.colors.background,
|
||||
foreground: theme.colors.textPrimary,
|
||||
},
|
||||
}}
|
||||
onRenderProcessGone={() => {
|
||||
console.log('WebView render process gone -> clear()');
|
||||
const xr = xtermRef.current;
|
||||
if (xr) xr.clear();
|
||||
}}
|
||||
onContentProcessDidTerminate={() => {
|
||||
console.log('WKWebView content process terminated -> clear()');
|
||||
const xr = xtermRef.current;
|
||||
if (xr) xr.clear();
|
||||
}}
|
||||
onLoadEnd={() => {
|
||||
console.log('WebView onLoadEnd');
|
||||
}}
|
||||
onMessage={(m) => {
|
||||
console.log('received msg', m);
|
||||
if (m.type === 'initialized') {
|
||||
if (terminalReadyRef.current) return;
|
||||
terminalReadyRef.current = true;
|
||||
onInitialized={() => {
|
||||
if (terminalReadyRef.current) return;
|
||||
terminalReadyRef.current = true;
|
||||
|
||||
// Replay from head, then attach live listener
|
||||
if (shell) {
|
||||
void (async () => {
|
||||
const res = await shell.readBuffer({ mode: 'head' });
|
||||
console.log('readBuffer(head)', {
|
||||
chunks: res.chunks.length,
|
||||
nextSeq: res.nextSeq,
|
||||
dropped: res.dropped,
|
||||
});
|
||||
if (res.chunks.length) {
|
||||
const chunks = res.chunks.map((c) => c.bytes);
|
||||
const xr = xtermRef.current;
|
||||
if (xr) {
|
||||
xr.writeMany(chunks);
|
||||
xr.flush();
|
||||
}
|
||||
}
|
||||
const id = shell.addListener(
|
||||
(ev: ListenerEvent) => {
|
||||
if ('kind' in ev) {
|
||||
console.log('listener.dropped', ev);
|
||||
return;
|
||||
}
|
||||
const chunk = ev;
|
||||
const xr3 = xtermRef.current;
|
||||
if (xr3) xr3.write(chunk.bytes);
|
||||
},
|
||||
{ cursor: { mode: 'seq', seq: res.nextSeq } },
|
||||
);
|
||||
console.log('shell listener attached', id.toString());
|
||||
listenerIdRef.current = id;
|
||||
})();
|
||||
}
|
||||
|
||||
// Focus to pop the keyboard (iOS needs the prop we set)
|
||||
const xr2 = xtermRef.current;
|
||||
if (xr2) xr2.focus();
|
||||
return;
|
||||
}
|
||||
if (m.type === 'data') {
|
||||
console.log('xterm->SSH', { len: m.data.length });
|
||||
const { buffer, byteOffset, byteLength } = m.data;
|
||||
const ab = buffer.slice(byteOffset, byteOffset + byteLength);
|
||||
if (shell) {
|
||||
shell.sendData(ab as ArrayBuffer).catch((e: unknown) => {
|
||||
console.warn('sendData failed', e);
|
||||
router.back();
|
||||
// Replay from head, then attach live listener
|
||||
if (shell) {
|
||||
void (async () => {
|
||||
const res = await shell.readBuffer({ mode: 'head' });
|
||||
console.log('readBuffer(head)', {
|
||||
chunks: res.chunks.length,
|
||||
nextSeq: res.nextSeq,
|
||||
dropped: res.dropped,
|
||||
});
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
console.log('xterm.debug', m.message);
|
||||
if (res.chunks.length) {
|
||||
const chunks = res.chunks.map((c) => c.bytes);
|
||||
const xr = xtermRef.current;
|
||||
if (xr) {
|
||||
xr.writeMany(chunks);
|
||||
xr.flush();
|
||||
}
|
||||
}
|
||||
const id = shell.addListener(
|
||||
(ev: ListenerEvent) => {
|
||||
if ('kind' in ev) {
|
||||
console.log('listener.dropped', ev);
|
||||
return;
|
||||
}
|
||||
const chunk = ev;
|
||||
const xr3 = xtermRef.current;
|
||||
if (xr3) xr3.write(chunk.bytes);
|
||||
},
|
||||
{ cursor: { mode: 'seq', seq: res.nextSeq } },
|
||||
);
|
||||
console.log('shell listener attached', id.toString());
|
||||
listenerIdRef.current = id;
|
||||
})();
|
||||
}
|
||||
|
||||
// Focus to pop the keyboard (iOS needs the prop we set)
|
||||
const xr2 = xtermRef.current;
|
||||
if (xr2) xr2.focus();
|
||||
return;
|
||||
}}
|
||||
onData={(terminalMessage) => {
|
||||
if (!shell) return;
|
||||
const bytes = encoder.encode(terminalMessage);
|
||||
if (shell) {
|
||||
shell.sendData(bytes.buffer).catch((e: unknown) => {
|
||||
console.warn('sendData failed', e);
|
||||
router.back();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
|
||||
Reference in New Issue
Block a user