From 8b381043733aef4a804c11989476674dee7fd6ac Mon Sep 17 00:00:00 2001
From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com>
Date: Sat, 4 Oct 2025 21:03:01 -0400
Subject: [PATCH] stuff
---
apps/mobile/src/app/(tabs)/shell/detail.tsx | 65 +++--
apps/mobile/src/app/(tabs)/shell/index.tsx | 259 +++++++++++++-----
apps/mobile/src/lib/ssh-store.ts | 6 +-
.../index.dev.html | 18 ++
.../src-internal/dev.ts | 14 +
.../src-internal/main.tsx | 14 +-
.../vite.config.internal.ts | 14 +-
7 files changed, 286 insertions(+), 104 deletions(-)
create mode 100644 packages/react-native-xtermjs-webview/index.dev.html
create mode 100644 packages/react-native-xtermjs-webview/src-internal/dev.ts
diff --git a/apps/mobile/src/app/(tabs)/shell/detail.tsx b/apps/mobile/src/app/(tabs)/shell/detail.tsx
index 48c1a77..4814ffb 100644
--- a/apps/mobile/src/app/(tabs)/shell/detail.tsx
+++ b/apps/mobile/src/app/(tabs)/shell/detail.tsx
@@ -20,7 +20,14 @@ import React, {
useRef,
useState,
} from 'react';
-import { KeyboardAvoidingView, Pressable, Text, View } from 'react-native';
+import {
+ KeyboardAvoidingView,
+ Pressable,
+ Text,
+ View,
+ type StyleProp,
+ type ViewStyle,
+} from 'react-native';
import { rootLogger } from '@/lib/logger';
import { useSshStore } from '@/lib/ssh-store';
import { useTheme } from '@/lib/theme';
@@ -165,19 +172,27 @@ function ShellDetail() {
headerBackVisible: true,
headerRight: () => (
{
- logger.info('Disconnect button pressed');
- if (!connection) return;
+ logger.info('Close Shell button pressed');
+ if (!shell) return;
try {
- await connection.disconnect();
+ await shell.close();
} catch (e) {
- logger.warn('Failed to disconnect', e);
+ logger.warn('Failed to close shell', e);
}
}}
+ style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}
>
-
+
+
+ Close Shell
+
),
}}
@@ -284,10 +299,13 @@ const KeyboardToolBarContext = createContext(
);
function KeyboardToolbar() {
+ const theme = useTheme();
return (
@@ -338,11 +356,16 @@ type KeyboardToolbarButtonPresetType =
function KeyboardToolbarButtonPreset({
preset,
+ style,
}: {
+ style?: StyleProp;
preset: KeyboardToolbarButtonPresetType;
}) {
return (
-
+
);
}
@@ -443,7 +466,10 @@ type KeyboardToolbarButtonProps =
| KeyboardToolbarModifierButtonProps
| KeyboardToolbarInstantButtonProps;
-function KeyboardToolbarButton(props: KeyboardToolbarButtonProps) {
+function KeyboardToolbarButton({
+ style,
+ ...props
+}: KeyboardToolbarButtonProps & { style?: StyleProp }) {
const theme = useTheme();
const { sendBytes, modifierKeysActive, setModifierKeysActive } =
useContextSafe(KeyboardToolBarContext);
@@ -464,6 +490,17 @@ function KeyboardToolbarButton(props: KeyboardToolbarButtonProps) {
return (
{
if (props.type === 'modifier') {
setModifierKeysActive((modifierKeysActive) =>
@@ -480,16 +517,6 @@ function KeyboardToolbarButton(props: KeyboardToolbarButtonProps) {
}
throw new Error('Invalid button type');
}}
- style={[
- {
- flex: 1,
- alignItems: 'center',
- justifyContent: 'center',
- borderWidth: 1,
- borderColor: theme.colors.border,
- },
- modifierActive && { backgroundColor: theme.colors.primary },
- ]}
>
{children}
diff --git a/apps/mobile/src/app/(tabs)/shell/index.tsx b/apps/mobile/src/app/(tabs)/shell/index.tsx
index 5b394cd..3f52abb 100644
--- a/apps/mobile/src/app/(tabs)/shell/index.tsx
+++ b/apps/mobile/src/app/(tabs)/shell/index.tsx
@@ -1,4 +1,4 @@
-import { Ionicons } from '@expo/vector-icons';
+import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
import {
type SshShell,
type SshConnection,
@@ -14,6 +14,8 @@ import {
Pressable,
Text,
View,
+ type StyleProp,
+ type TextStyle,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useShallow } from 'zustand/react/shallow';
@@ -22,6 +24,7 @@ import { preferences } from '@/lib/preferences';
import {} from '@/lib/query-fns';
import { useSshStore } from '@/lib/ssh-store';
import { useTheme } from '@/lib/theme';
+import { AbortSignalTimeout } from '@/lib/utils';
const logger = rootLogger.extend('TabsShellList');
@@ -67,6 +70,8 @@ function LoadedState() {
const [shellListViewMode] =
preferences.shellListViewMode.useShellListViewModePref();
+ const router = useRouter();
+
return (
{shellListViewMode === 'flat' ? (
@@ -74,8 +79,8 @@ function LoadedState() {
) : (
)}
- {
setActionTarget(null);
}}
@@ -83,11 +88,40 @@ function LoadedState() {
if (!actionTarget) return;
if (!('shell' in actionTarget)) return;
void actionTarget.shell.close();
+ setActionTarget(null);
+ }}
+ />
+ {
+ setActionTarget(null);
}}
onDisconnect={() => {
if (!actionTarget) return;
if (!('connection' in actionTarget)) return;
void actionTarget.connection.disconnect();
+ setActionTarget(null);
+ }}
+ onStartShell={() => {
+ if (!actionTarget) return;
+ if (!('connection' in actionTarget)) return;
+ void actionTarget.connection
+ .startShell({
+ term: 'Xterm',
+ abortSignal: AbortSignalTimeout(5_000),
+ })
+ .then((shellHandle) => {
+ router.push({
+ pathname: '/shell/detail',
+ params: {
+ connectionId: actionTarget.connection.connectionId,
+ channelId: shellHandle.channelId,
+ },
+ });
+ });
+ setActionTarget(null);
}}
/>
@@ -165,6 +199,11 @@ function GroupedView({
[item.connectionId]: !prev[item.connectionId],
}));
}}
+ onLongPress={() => {
+ setActionTarget({
+ connection: item,
+ });
+ }}
>
void;
onCloseShell: () => void;
+}) {
+ const open = !!target;
+
+ return (
+
+ );
+}
+
+function ConnectionActionsSheet({
+ target,
+ onClose,
+ onDisconnect,
+ onStartShell,
+}: {
+ target: null | {
+ connection: SshConnection;
+ };
+ onClose: () => void;
onDisconnect: () => void;
+ onStartShell: () => void;
+}) {
+ const open = !!target;
+
+ return (
+
+ );
+}
+
+type ActionSheetButtonVariant = 'primary' | 'outline';
+
+type ActionSheetAction = {
+ label: string;
+ onPress: () => void;
+ variant?: ActionSheetButtonVariant;
+};
+
+function ActionSheetModal({
+ open,
+ title,
+ onClose,
+ actions,
+ extraFooterSpacing = 0,
+}: {
+ open: boolean;
+ title: string;
+ onClose: () => void;
+ actions: ActionSheetAction[];
+ extraFooterSpacing?: number;
}) {
const theme = useTheme();
- const open = !!target;
+
return (
- Shell Actions
+ {title}
-
-
- Close Shell
-
-
-
-
-
- Disconnect Connection
-
-
-
-
-
- Cancel
-
-
+ {actions.map((action, index) => (
+
+
+ {index < actions.length - 1 ? (
+
+ ) : null}
+
+ ))}
+ {extraFooterSpacing > 0 ? (
+
+ ) : null}
);
}
+function ActionSheetButton({
+ label,
+ onPress,
+ variant = 'primary',
+}: ActionSheetAction) {
+ const theme = useTheme();
+ const baseButtonStyle = {
+ borderRadius: 12,
+ paddingVertical: 14,
+ alignItems: 'center',
+ } as const;
+
+ const pressableStyle =
+ variant === 'outline'
+ ? [
+ baseButtonStyle,
+ {
+ backgroundColor: theme.colors.transparent,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ },
+ ]
+ : [baseButtonStyle, { backgroundColor: theme.colors.primary }];
+
+ const textStyle: StyleProp =
+ variant === 'outline'
+ ? {
+ color: theme.colors.textSecondary,
+ fontWeight: '600',
+ fontSize: 14,
+ letterSpacing: 0.3,
+ }
+ : {
+ color: theme.colors.buttonTextOnPrimary,
+ fontWeight: '700',
+ fontSize: 14,
+ letterSpacing: 0.3,
+ };
+
+ return (
+
+ {label}
+
+ );
+}
+
function HeaderViewModeButton() {
const theme = useTheme();
const [shellListViewMode, setShellListViewMode] =
preferences.shellListViewMode.useShellListViewModePref();
- const icon = shellListViewMode === 'flat' ? 'list' : 'git-branch';
const accessibilityLabel =
shellListViewMode === 'flat'
? 'Switch to grouped view'
@@ -480,7 +575,19 @@ function HeaderViewModeButton() {
opacity: pressed ? 0.4 : 1,
})}
>
-
+ {shellListViewMode === 'grouped' ? (
+
+ ) : (
+
+ )}
);
}
diff --git a/apps/mobile/src/lib/ssh-store.ts b/apps/mobile/src/lib/ssh-store.ts
index af1257a..5e980bb 100644
--- a/apps/mobile/src/lib/ssh-store.ts
+++ b/apps/mobile/src/lib/ssh-store.ts
@@ -39,9 +39,9 @@ export const useSshStore = create((set) => ({
logger.debug('shell closed', storeKey);
set((s) => {
const { [storeKey]: _omit, ...rest } = s.shells;
- if (Object.keys(rest).length === 0) {
- void connection.disconnect();
- }
+ // if (Object.keys(rest).length === 0) {
+ // void connection.disconnect();
+ // }
return { shells: rest };
});
},
diff --git a/packages/react-native-xtermjs-webview/index.dev.html b/packages/react-native-xtermjs-webview/index.dev.html
new file mode 100644
index 0000000..cb6cbba
--- /dev/null
+++ b/packages/react-native-xtermjs-webview/index.dev.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/react-native-xtermjs-webview/src-internal/dev.ts b/packages/react-native-xtermjs-webview/src-internal/dev.ts
new file mode 100644
index 0000000..7f7acb6
--- /dev/null
+++ b/packages/react-native-xtermjs-webview/src-internal/dev.ts
@@ -0,0 +1,14 @@
+// This file is only loaded in dev mode.
+
+// These lines should replicate injectedJavaScriptBeforeContentLoaded from src/index.tsx
+document.body.style.backgroundColor = '#0B1324';
+
+// Replicate injectedJavaScriptObject from src/index.tsx
+window.ReactNativeWebView = {
+ postMessage: (data: string) => {
+ console.log('postMessage', data);
+ },
+ injectedObjectJson: () => {
+ return JSON.stringify({});
+ },
+};
diff --git a/packages/react-native-xtermjs-webview/src-internal/main.tsx b/packages/react-native-xtermjs-webview/src-internal/main.tsx
index 0649b86..b07b820 100644
--- a/packages/react-native-xtermjs-webview/src-internal/main.tsx
+++ b/packages/react-native-xtermjs-webview/src-internal/main.tsx
@@ -153,7 +153,19 @@ window.onload = () => {
window.addEventListener('message', handler);
// Initial handshake (send once)
- setTimeout(() => sendToRn({ type: 'initialized' }), 100);
+ setTimeout(() => {
+ const ta = document.querySelector(
+ '.xterm-helper-textarea',
+ ) as HTMLTextAreaElement | null;
+ if (!ta) throw new Error('xterm-helper-textarea not found');
+ ta.setAttribute('autocomplete', 'off');
+ ta.setAttribute('autocorrect', 'off');
+ ta.setAttribute('autocapitalize', 'none');
+ ta.setAttribute('spellcheck', 'false');
+ ta.setAttribute('inputmode', 'verbatim');
+
+ return sendToRn({ type: 'initialized' });
+ }, 200);
} catch (e) {
sendToRn({
type: 'debug',
diff --git a/packages/react-native-xtermjs-webview/vite.config.internal.ts b/packages/react-native-xtermjs-webview/vite.config.internal.ts
index 958bf7c..3cc04dd 100644
--- a/packages/react-native-xtermjs-webview/vite.config.internal.ts
+++ b/packages/react-native-xtermjs-webview/vite.config.internal.ts
@@ -2,9 +2,13 @@ import { defineConfig } from 'vite';
import { viteSingleFile } from 'vite-plugin-singlefile';
// https://vite.dev/config/
-export default defineConfig({
- plugins: [viteSingleFile()],
- build: {
- outDir: 'dist-internal',
- },
+export default defineConfig((ctx) => {
+ const input = ctx.command === 'serve' ? 'index.html' : 'index.build.html';
+ console.log('Vite Internal Working with input', input);
+ return {
+ plugins: [viteSingleFile()],
+ build: {
+ outDir: 'dist-internal',
+ },
+ };
});