mirror of
https://github.com/EthanShoeDev/fressh.git
synced 2026-01-11 14:22:51 +00:00
theme
This commit is contained in:
214
apps/mobile/src/app/(tabs)/shell/detail.tsx
Normal file
214
apps/mobile/src/app/(tabs)/shell/detail.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { RnRussh } from '@fressh/react-native-uniffi-russh';
|
||||
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '@/lib/theme';
|
||||
|
||||
export default function TabsShellDetail() {
|
||||
return <ShellDetail />;
|
||||
}
|
||||
|
||||
function ShellDetail() {
|
||||
const { connectionId, channelId } = useLocalSearchParams<{
|
||||
connectionId?: string;
|
||||
channelId?: string;
|
||||
}>();
|
||||
const router = useRouter();
|
||||
const theme = useTheme();
|
||||
|
||||
const channelIdNum = Number(channelId);
|
||||
const connection = connectionId
|
||||
? RnRussh.getSshConnection(String(connectionId))
|
||||
: undefined;
|
||||
const shell =
|
||||
connectionId && channelId
|
||||
? RnRussh.getSshShell(String(connectionId), channelIdNum)
|
||||
: undefined;
|
||||
|
||||
const [shellData, setShellData] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!connection) return;
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const listenerId = connection.addChannelListener((data: ArrayBuffer) => {
|
||||
try {
|
||||
const bytes = new Uint8Array(data);
|
||||
const chunk = decoder.decode(bytes);
|
||||
setShellData((prev) => prev + chunk);
|
||||
} catch (e) {
|
||||
console.warn('Failed to decode shell data', e);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
connection.removeChannelListener(listenerId);
|
||||
};
|
||||
}, [connection]);
|
||||
|
||||
const scrollViewRef = useRef<ScrollView | null>(null);
|
||||
useEffect(() => {
|
||||
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||
}, [shellData]);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: theme.colors.background }}>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerLeft: () => (
|
||||
<Pressable
|
||||
onPress={() => router.back()}
|
||||
hitSlop={10}
|
||||
style={{ paddingHorizontal: 4, paddingVertical: 4 }}
|
||||
>
|
||||
<Ionicons
|
||||
name="chevron-back"
|
||||
size={22}
|
||||
color={theme.colors.textPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
),
|
||||
headerRight: () => (
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
try {
|
||||
await connection?.disconnect();
|
||||
} catch {}
|
||||
router.replace('/shell');
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.colors.primary, fontWeight: '700' }}>
|
||||
Disconnect
|
||||
</Text>
|
||||
</Pressable>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={[styles.container, { backgroundColor: theme.colors.background }]}
|
||||
>
|
||||
<View style={styles.terminal}>
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
contentContainerStyle={styles.terminalContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<Text selectable style={styles.terminalText}>
|
||||
{shellData || 'Connected. Output will appear here...'}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<CommandInput
|
||||
executeCommand={async (command) => {
|
||||
await shell?.sendData(
|
||||
Uint8Array.from(new TextEncoder().encode(command + '\n')).buffer,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
<TextInput
|
||||
testID="command-input"
|
||||
style={styles.commandInput}
|
||||
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={[styles.executeButton, { marginTop: 8 }]}
|
||||
onPress={handleExecute}
|
||||
testID="execute-button"
|
||||
>
|
||||
<Text style={styles.executeButtonText}>Execute</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0B1324',
|
||||
padding: 16,
|
||||
},
|
||||
terminal: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0E172B',
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#2A3655',
|
||||
overflow: 'hidden',
|
||||
marginBottom: 12,
|
||||
},
|
||||
terminalContent: {
|
||||
padding: 12,
|
||||
},
|
||||
terminalText: {
|
||||
color: '#D1D5DB',
|
||||
fontSize: 14,
|
||||
lineHeight: 18,
|
||||
fontFamily: Platform.select({
|
||||
ios: 'Menlo',
|
||||
android: 'monospace',
|
||||
default: 'monospace',
|
||||
}),
|
||||
},
|
||||
commandInput: {
|
||||
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',
|
||||
}),
|
||||
},
|
||||
executeButton: {
|
||||
backgroundColor: '#2563EB',
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
executeButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '700',
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user