diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx
index 569e6c4..4e3fb78 100644
--- a/apps/mobile/src/app/(tabs)/index.tsx
+++ b/apps/mobile/src/app/(tabs)/index.tsx
@@ -21,7 +21,7 @@ import {
type InputConnectionDetails,
} from '@/lib/secrets-manager';
import { useTheme } from '@/lib/theme';
-import { useBottomTabPadding } from '@/lib/useBottomTabPadding';
+import { useBottomTabSpacing } from '@/lib/useBottomTabSpacing';
export default function TabsIndex() {
return ;
@@ -45,7 +45,7 @@ function Host() {
const sshConnMutation = useSshConnMutation({
onConnectionProgress: (s) => setLastConnectionProgressEvent(s),
});
- const { paddingBottom, onLayout } = useBottomTabPadding(12);
+ const marginBottom = useBottomTabSpacing();
const connectionForm = useAppForm({
// https://tanstack.com/form/latest/docs/framework/react/guides/async-initial-values
defaultValues,
@@ -86,10 +86,9 @@ function Host() {
return (
{
- const extra = windowH - (y + height);
- return extra > 0 ? extra : 0;
- };
-
- // Measure any bottom overlap (e.g., native tab bar) and add padding to avoid it
- const [bottomExtra, setBottomExtra] = useState(0);
+ const marginBottom = useBottomTabSpacing();
return (
- {
- const { y, height } = e.nativeEvent.layout;
- const extra = computeBottomExtra(y, height);
- if (extra !== bottomExtra) setBottomExtra(extra);
- }}
- style={{
- flex: 1,
- justifyContent: 'flex-start',
- backgroundColor: theme.colors.background,
- paddingTop: 2,
- paddingLeft: 8,
- paddingRight: 8,
- paddingBottom: insets.bottom + (bottomExtra || estimatedTabBarHeight),
- }}
- >
- (
- {
- if (!connection) return;
- try {
- await connection.disconnect();
- } catch (e) {
- console.warn('Failed to disconnect', e);
- }
- }}
- >
-
-
- ),
+ <>
+
- {
- if (terminalReadyRef.current) return;
- terminalReadyRef.current = true;
-
- if (!shell) throw new Error('Shell not found');
-
- // Replay from head, then attach live listener
- void (async () => {
- const res = 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.map((c) => new Uint8Array(c)));
- 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(new Uint8Array(chunk.bytes));
+ >
+ (
+ {
+ if (!connection) return;
+ try {
+ await connection.disconnect();
+ } catch (e) {
+ console.warn('Failed to disconnect', e);
+ }
+ }}
+ >
+
+
+ ),
+ }}
+ />
+
+ {
- if (!shell) return;
- const bytes = encoder.encode(terminalMessage);
- shell.sendData(bytes.buffer).catch((e: unknown) => {
- console.warn('sendData failed', e);
- router.back();
- });
+ }}
+ onInitialized={() => {
+ if (terminalReadyRef.current) return;
+ terminalReadyRef.current = true;
+
+ if (!shell) throw new Error('Shell not found');
+
+ // Replay from head, then attach live listener
+ void (async () => {
+ const res = 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.map((c) => new Uint8Array(c)));
+ 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(new Uint8Array(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();
+ }}
+ onData={(terminalMessage) => {
+ if (!shell) return;
+ const bytes = encoder.encode(terminalMessage);
+ shell.sendData(bytes.buffer).catch((e: unknown) => {
+ console.warn('sendData failed', e);
+ router.back();
+ });
+ }}
+ />
+
+
+
-
+ >
);
}
diff --git a/apps/mobile/src/app/(test)/_layout.tsx b/apps/mobile/src/app/(test)/_layout.tsx
index 9fb5533..46fcf24 100644
--- a/apps/mobile/src/app/(test)/_layout.tsx
+++ b/apps/mobile/src/app/(test)/_layout.tsx
@@ -9,6 +9,7 @@ import { useTheme } from '@/lib/theme';
export default function TabsLayout() {
const theme = useTheme();
+
return (
-
+
>
);
}
@@ -62,15 +66,21 @@ const TextInputAndLabel = (props: CustomTextInputProps) => {
const { title, ...rest } = props;
const [isFocused, setFocused] = useState(false);
- const onFocus = useCallback>((e) => {
- setFocused(true);
- props.onFocus?.(e);
- }, []);
+ const onFocus = useCallback>(
+ (e) => {
+ setFocused(true);
+ props.onFocus?.(e);
+ },
+ [props],
+ );
- const onBlur = useCallback>((e) => {
- setFocused(false);
- props.onBlur?.(e);
- }, []);
+ const onBlur = useCallback>(
+ (e) => {
+ setFocused(false);
+ props.onBlur?.(e);
+ },
+ [props],
+ );
return (
<>
diff --git a/apps/mobile/src/app/index.tsx b/apps/mobile/src/app/index.tsx
index 7d618d5..d197633 100644
--- a/apps/mobile/src/app/index.tsx
+++ b/apps/mobile/src/app/index.tsx
@@ -1,5 +1,5 @@
import { Redirect } from 'expo-router';
export default function RootRedirect() {
- return ;
+ return ;
}
diff --git a/apps/mobile/src/lib/useBottomTabPadding.ts b/apps/mobile/src/lib/useBottomTabPadding.ts
deleted file mode 100644
index 06ef485..0000000
--- a/apps/mobile/src/lib/useBottomTabPadding.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import { Dimensions, Platform } from 'react-native';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
-
-type LayoutEvent = {
- nativeEvent: {
- layout: {
- y: number;
- height: number;
- };
- };
-};
-
-export function useBottomTabPadding(basePadding = 12) {
- const insets = useSafeAreaInsets();
- const windowH = Dimensions.get('window').height;
- const estimatedTabBarHeight = Platform.select({
- ios: 49,
- android: 80,
- default: 56,
- });
- const [bottomExtra, setBottomExtra] = React.useState(0);
-
- const onLayout = React.useCallback(
- (e: LayoutEvent) => {
- const { y, height } = e.nativeEvent.layout;
- const extra = windowH - (y + height);
- setBottomExtra(extra > 0 ? extra : 0);
- },
- [windowH],
- );
-
- const paddingBottom =
- basePadding + insets.bottom + (bottomExtra || estimatedTabBarHeight!);
- return { paddingBottom, onLayout } as const;
-}
diff --git a/apps/mobile/src/lib/useBottomTabSpacing.ts b/apps/mobile/src/lib/useBottomTabSpacing.ts
new file mode 100644
index 0000000..e00c341
--- /dev/null
+++ b/apps/mobile/src/lib/useBottomTabSpacing.ts
@@ -0,0 +1,12 @@
+import { Platform } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+export function useBottomTabSpacing() {
+ const insets = useSafeAreaInsets();
+ const estimatedTabBarHeight = Platform.select({
+ ios: 49,
+ android: 80,
+ default: 56,
+ });
+ return insets.bottom + estimatedTabBarHeight;
+}
diff --git a/packages/react-native-xtermjs-webview/src-internal/main.tsx b/packages/react-native-xtermjs-webview/src-internal/main.tsx
index 27092e9..0649b86 100644
--- a/packages/react-native-xtermjs-webview/src-internal/main.tsx
+++ b/packages/react-native-xtermjs-webview/src-internal/main.tsx
@@ -153,7 +153,7 @@ window.onload = () => {
window.addEventListener('message', handler);
// Initial handshake (send once)
- setTimeout(() => sendToRn({ type: 'initialized' }), 50);
+ setTimeout(() => sendToRn({ type: 'initialized' }), 100);
} catch (e) {
sendToRn({
type: 'debug',