some things working

This commit is contained in:
EthanShoeDev
2025-09-17 16:44:16 -04:00
parent b3eb42c348
commit 86ff6762a3
8 changed files with 125 additions and 189 deletions

View File

@@ -5,22 +5,15 @@ import {
type XtermWebViewHandle,
} from '@fressh/react-native-xtermjs-webview';
import { useQueryClient } from '@tanstack/react-query';
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
import React, { useEffect, useRef, useState } from 'react';
import {
Platform,
Pressable,
ScrollView,
Text,
TextInput,
View,
} from 'react-native';
import PQueue from 'p-queue';
import React, { useEffect, useRef } from 'react';
import { Pressable, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { disconnectSshConnectionAndInvalidateQuery } from '@/lib/query-fns';
import { useTheme } from '@/lib/theme';
const renderer: 'xtermjs' | 'rn-text' = 'xtermjs';
const decoder = new TextDecoder('utf-8');
export default function TabsShellDetail() {
return <ShellDetail />;
}
@@ -43,29 +36,42 @@ function ShellDetail() {
? RnRussh.getSshShell(String(connectionId), channelIdNum)
: undefined;
const [shellData, setShellData] = useState('');
function sendDataToXterm(data: ArrayBuffer) {
try {
const bytes = new Uint8Array(data);
console.log('sendDataToXterm', new TextDecoder().decode(bytes));
xtermWebViewRef.current?.write(bytes);
} catch (e) {
console.warn('Failed to decode shell data', e);
}
}
const queueRef = useRef<PQueue>(null);
useEffect(() => {
if (!connection) return;
if (!queueRef.current)
queueRef.current = new PQueue({
concurrency: 1,
intervalCap: 1, // <= one task per interval
interval: 100, // <= 100ms between tasks
autoStart: false, // <= buffer until we start()
});
const xtermQueue = queueRef.current;
if (!connection || !xtermQueue) return;
const listenerId = connection.addChannelListener((data: ArrayBuffer) => {
try {
const bytes = new Uint8Array(data);
xtermWebViewRef.current?.write(bytes);
const chunk = decoder.decode(bytes);
setShellData((prev) => prev + chunk);
} catch (e) {
console.warn('Failed to decode shell data', e);
}
console.log('ssh.onData', new TextDecoder().decode(new Uint8Array(data)));
void xtermQueue.add(() => {
sendDataToXterm(data);
});
});
return () => {
connection.removeChannelListener(listenerId);
xtermQueue.pause();
xtermQueue.clear();
};
}, [connection]);
}, [connection, queueRef]);
const scrollViewRef = useRef<ScrollView | null>(null);
useEffect(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, [shellData]);
const queryClient = useQueryClient();
return (
<SafeAreaView style={{ flex: 1, backgroundColor: theme.colors.background }}>
@@ -77,9 +83,15 @@ function ShellDetail() {
accessibilityLabel="Disconnect"
hitSlop={10}
onPress={async () => {
if (!connection) return;
try {
await connection?.disconnect();
} catch {}
await disconnectSshConnectionAndInvalidateQuery({
connectionId: connection.connectionId,
queryClient: queryClient,
});
} catch (e) {
console.warn('Failed to disconnect', e);
}
router.replace('/shell');
}}
>
@@ -94,140 +106,30 @@ function ShellDetail() {
{ backgroundColor: theme.colors.background },
]}
>
<ScrollView>
{renderer === 'xtermjs' ? (
<XtermJsWebView
ref={xtermWebViewRef}
style={{ flex: 1, height: 400 }}
// textZoom={0}
// injectedJavaScript={`
// setTimeout(() => {
// document.body.style.backgroundColor = '${theme.colors.background}';
// document.body.style.color = '${theme.colors.textPrimary}';
// document.body.style.fontSize = '80px';
// const termDiv = document.getElementById('terminal');
// termDiv.style.backgroundColor = '${theme.colors.background}';
// termDiv.style.color = '${theme.colors.textPrimary}';
// window.terminal.options.fontSize = 50;
// }, 50);
// `}
onMessage={(event) => {
console.log('onMessage', event.nativeEvent.data);
}}
/>
) : (
<View
style={{
flex: 1,
backgroundColor: '#0E172B',
borderRadius: 12,
height: 400,
borderWidth: 1,
borderColor: '#2A3655',
overflow: 'hidden',
marginBottom: 12,
}}
>
<ScrollView
ref={scrollViewRef}
contentContainerStyle={{
paddingHorizontal: 12,
paddingTop: 4,
paddingBottom: 12,
}}
keyboardShouldPersistTaps="handled"
>
<Text
selectable
style={{
color: '#D1D5DB',
fontSize: 14,
lineHeight: 18,
fontFamily: Platform.select({
ios: 'Menlo',
android: 'monospace',
default: 'monospace',
}),
}}
>
{shellData || 'Connected. Output will appear here...'}
</Text>
</ScrollView>
</View>
)}
<CommandInput
executeCommand={async (command) => {
await shell?.sendData(
Uint8Array.from(new TextEncoder().encode(command + '\n'))
.buffer,
);
}}
/>
</ScrollView>
<XtermJsWebView
ref={xtermWebViewRef}
style={{ flex: 1, height: 400 }}
// textZoom={0}
injectedJavaScript={`
document.body.style.backgroundColor = '${theme.colors.background}';
const termDiv = document.getElementById('terminal');
window.terminal.options.fontSize = 50;
setTimeout(() => {
window.fitAddon?.fit();
}, 1_000);
`}
onMessage={(message) => {
if (message.type === 'initialized') {
console.log('xterm.onMessage initialized');
queueRef.current?.start();
return;
}
const data = message.data;
console.log('xterm.onMessage', new TextDecoder().decode(data));
void shell?.sendData(data.buffer as ArrayBuffer);
}}
/>
</View>
</SafeAreaView>
);
}
function CommandInput(props: {
executeCommand: (command: string) => Promise<void>;
}) {
const [command, setCommand] = useState('');
async function handleExecute() {
if (!command.trim()) return;
await props.executeCommand(command);
setCommand('');
}
return (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<TextInput
testID="command-input"
style={{
flex: 1,
backgroundColor: '#0E172B',
borderWidth: 1,
borderColor: '#2A3655',
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 12,
color: '#E5E7EB',
fontSize: 16,
fontFamily: Platform.select({
ios: 'Menlo',
android: 'monospace',
default: 'monospace',
}),
}}
value={command}
onChangeText={setCommand}
placeholder="Type a command and press Enter or Execute"
placeholderTextColor="#9AA0A6"
autoCapitalize="none"
autoCorrect={false}
returnKeyType="send"
onSubmitEditing={handleExecute}
/>
<Pressable
style={[
{
backgroundColor: '#2563EB',
borderRadius: 10,
paddingHorizontal: 16,
paddingVertical: 12,
alignItems: 'center',
justifyContent: 'center',
},
{ marginTop: 8 },
]}
onPress={handleExecute}
testID="execute-button"
>
<Text style={{ color: '#FFFFFF', fontWeight: '700', fontSize: 14 }}>
Execute
</Text>
</Pressable>
</View>
);
}