Some working buttons

This commit is contained in:
EthanShoeDev
2025-10-04 14:40:45 -04:00
parent c0c73f20fa
commit adbd87d102
3 changed files with 260 additions and 66 deletions

View File

@@ -11,16 +11,20 @@ import {
useRouter,
useFocusEffect,
} from 'expo-router';
import React, { startTransition, useEffect, useRef, useState } from 'react';
import React, {
createContext,
startTransition,
useEffect,
useRef,
useState,
} from 'react';
import { KeyboardAvoidingView, Pressable, Text, View } from 'react-native';
// import {
// // KeyboardAvoidingView,
// KeyboardToolbar,
// } from 'react-native-keyboard-controller';
import { useSshStore } from '@/lib/ssh-store';
import { useTheme } from '@/lib/theme';
import { useBottomTabSpacing } from '@/lib/useBottomTabSpacing';
import { useContextSafe } from '@/lib/utils';
type IconName = keyof typeof Ionicons.glyphMap;
export default function TabsShellDetail() {
const [ready, setReady] = useState(false);
@@ -145,7 +149,14 @@ function ShellDetail() {
<KeyboardAvoidingView
behavior="height"
keyboardVerticalOffset={120}
style={{ flex: 1, borderWidth: 2, borderColor: theme.colors.border }}
style={{ flex: 1, gap: 4 }}
>
<View
style={{
flex: 1,
borderWidth: 2,
borderColor: theme.colors.border,
}}
>
<XtermJsWebView
ref={xtermRef}
@@ -217,6 +228,16 @@ function ShellDetail() {
});
}}
/>
</View>
<KeyboardToolbar
sendBytes={(bytes) => {
if (!shell) return;
shell.sendData(bytes.buffer).catch((e: unknown) => {
console.warn('sendData failed', e);
router.back();
});
}}
/>
</KeyboardAvoidingView>
</View>
{/* <KeyboardToolbar
@@ -227,3 +248,163 @@ function ShellDetail() {
</>
);
}
type KeyboardToolbarProps = {
sendBytes: (bytes: Uint8Array<ArrayBuffer>) => void;
};
const KeyboardToolBarContext = createContext<KeyboardToolbarProps | null>(null);
function KeyboardToolbar(props: KeyboardToolbarProps) {
return (
<KeyboardToolBarContext value={props}>
<View
style={{
height: 100,
}}
>
<KeyboardToolbarRow>
<KeyboardToolbarButtonPreset preset="esc" />
<KeyboardToolbarButtonPreset preset="/" />
<KeyboardToolbarButtonPreset preset="|" />
<KeyboardToolbarButtonPreset preset="home" />
<KeyboardToolbarButtonPreset preset="up" />
<KeyboardToolbarButtonPreset preset="end" />
<KeyboardToolbarButtonPreset preset="pgup" />
</KeyboardToolbarRow>
<KeyboardToolbarRow>
<KeyboardToolbarButtonPreset preset="tab" />
<KeyboardToolbarButtonPreset preset="ctrl" />
<KeyboardToolbarButtonPreset preset="alt" />
<KeyboardToolbarButtonPreset preset="left" />
<KeyboardToolbarButtonPreset preset="down" />
<KeyboardToolbarButtonPreset preset="right" />
<KeyboardToolbarButtonPreset preset="pgdn" />
</KeyboardToolbarRow>
</View>
</KeyboardToolBarContext>
);
}
function KeyboardToolbarRow({ children }: { children?: React.ReactNode }) {
return <View style={{ flexDirection: 'row', flex: 1 }}>{children}</View>;
}
type KeyboardToolbarButtonPresetType =
| 'esc'
| '/'
| '|'
| 'home'
| 'up'
| 'end'
| 'pgup'
| 'pgdn'
| 'fn'
| 'tab'
| 'ctrl'
| 'alt'
| 'left'
| 'down'
| 'right'
| 'insert'
| 'delete'
| 'pageup'
| 'pagedown'
| 'fn';
function KeyboardToolbarButtonPreset({
preset,
}: {
preset: KeyboardToolbarButtonPresetType;
}) {
return (
<KeyboardToolbarButton {...keyboardToolbarButtonPresetToProps[preset]} />
);
}
const keyboardToolbarButtonPresetToProps: Record<
KeyboardToolbarButtonPresetType,
KeyboardToolbarButtonProps
> = {
esc: { label: 'ESC', sendBytes: new Uint8Array([27]) },
'/': { label: '/', sendBytes: new Uint8Array([47]) },
'|': { label: '|', sendBytes: new Uint8Array([124]) },
home: { label: 'HOME', sendBytes: new Uint8Array([27, 91, 72]) },
end: { label: 'END', sendBytes: new Uint8Array([27, 91, 70]) },
pgup: { label: 'PGUP', sendBytes: new Uint8Array([27, 91, 53, 126]) },
pgdn: { label: 'PGDN', sendBytes: new Uint8Array([27, 91, 54, 126]) },
fn: { label: 'FN', isModifier: true },
tab: { label: 'TAB', sendBytes: new Uint8Array([9]) },
ctrl: { label: 'CTRL', isModifier: true },
alt: { label: 'ALT', isModifier: true },
left: { iconName: 'arrow-back', sendBytes: new Uint8Array([27, 91, 68]) },
up: { iconName: 'arrow-up', sendBytes: new Uint8Array([27, 91, 65]) },
down: { iconName: 'arrow-down', sendBytes: new Uint8Array([27, 91, 66]) },
right: {
iconName: 'arrow-forward',
sendBytes: new Uint8Array([27, 91, 67]),
},
insert: { label: 'INSERT', sendBytes: new Uint8Array([27, 91, 50, 126]) },
delete: { label: 'DELETE', sendBytes: new Uint8Array([27, 91, 51, 126]) },
pageup: { label: 'PAGEUP', sendBytes: new Uint8Array([27, 91, 53, 126]) },
pagedown: { label: 'PAGEDOWN', sendBytes: new Uint8Array([27, 91, 54, 126]) },
};
type KeyboardToolbarButtonProps = (
| {
isModifier: true;
}
| {
sendBytes: Uint8Array;
}
) &
(
| {
label: string;
}
| {
iconName: IconName;
}
);
function KeyboardToolbarButton(props: KeyboardToolbarButtonProps) {
const theme = useTheme();
const [modifierActive, setModifierActive] = useState(false);
const { sendBytes } = useContextSafe(KeyboardToolBarContext);
const isTextLabel = 'label' in props;
const children = isTextLabel ? (
<Text style={{ color: theme.colors.textPrimary }}>{props.label}</Text>
) : (
<Ionicons
name={props.iconName}
size={20}
color={theme.colors.textPrimary}
/>
);
return (
<Pressable
onPress={() => {
console.log('button pressed');
if ('isModifier' in props && props.isModifier) {
setModifierActive((active) => !active);
} else if ('sendBytes' in props) {
// todo: send key press
sendBytes(new Uint8Array(props.sendBytes));
}
}}
style={[
{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: theme.colors.border,
},
modifierActive && { backgroundColor: theme.colors.primary },
]}
>
{children}
</Pressable>
);
}

View File

@@ -36,6 +36,9 @@ export const useSshStore = create<SshRegistryStore>((set) => ({
console.log('DEBUG shell closed', storeKey);
set((s) => {
const { [storeKey]: _omit, ...rest } = s.shells;
if (Object.keys(rest).length === 0) {
void connection.disconnect();
}
return { shells: rest };
});
},

View File

@@ -1,4 +1,5 @@
import { QueryClient } from '@tanstack/react-query';
import { use, type Context } from 'react';
export const queryClient = new QueryClient();
@@ -13,3 +14,12 @@ export const AbortSignalTimeout = (timeout: number) => {
}, timeout);
return controller.signal;
};
export const useContextSafe = <T>(context: Context<T>) => {
const contextValue = use(context);
if (!contextValue) {
throw new Error('Context not found');
}
return contextValue;
};