Files
termix-mobile/app/contexts/KeyboardCustomizationContext.tsx
Luke Gustafson 6c40d2f0cc v1.4.0 (#31)
* fix: register physical Tab key on iPadOS/iOS hardware keyboards (#14)

The native module only registered Shift+Tab but not bare Tab, so iOS
intercepted it for UI focus navigation. Register an unmodified Tab
UIKeyCommand with wantsPriorityOverSystemBehavior and handle both
Tab and Shift+Tab in the same JS branch.

Fixes Termix-SSH/Support#557

* fix: pass jumpHosts, forceKeyboardInteractive, and overrideCredentialUsername to terminal connection (#15)

These fields were defined on SSHHost but omitted from the terminal
hostConfig passthrough. Without jumpHosts the backend cannot route
through jump servers; without forceKeyboardInteractive hosts that
require keyboard-interactive auth fail silently; without
overrideCredentialUsername shared credentials use the wrong username.

Fixes Termix-SSH/Support#422
Fixes Termix-SSH/Support#638

* fix: improve mobile terminal scroll recovery (#17)

* Add mobile remote desktop sessions (#18)

* feat: add mobile remote desktop sessions

* feat: add remote desktop input controls

* feat: polish remote desktop controls

* feat: add mobile terminal font selection (#19)

* feat: add mobile shift tab hotkey (#20)

* feat: initial UI redesign

* feat: revamp server login system

* chore: update app icon/background color

* fix: remove unused fields in the settings tab

* feat: improve the user pin system and update all settings modals to use new ui

* fix: active server not updating unless app restarts

* feat: add version in settings

* feat: improve host page UI (updated host form and added credential management)

* fix: preserve composed iOS terminal input

* ci: restore mobile checks

* feat: rewrite of connection system for all types

* fix: preserve mobile jump hosts (#30)

* fix: guard android user ca trust (#29)

* fix: capture ios hardware tab key (#27)

* fix: reset terminal input after special keys (#28)

* fix: open oidc in system browser (#26)

* fix: preserve websocket auth context (#25)

* fix: keep none-auth terminal prompts alive (#23)

* fix: allow local network SSL in Android app (#21)

* feat: allow oidc passkeys

* feat: bundeled xterm into app and added docker loading screen

* feat: add 2fa, api key, and active sessions to settings and addressed multiple bug fixes and inconsistencies with termix web

* chore: update repo images and readme

* chore: update readme to split table for screenshots

* chore: rename ios to apple in build workflow

* fix: android build failure

* feat: add snippet management and fix several bugs

* fix: web login and oidc incorrect logic

* chore: run linter

---------

Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 16:49:50 -05:00

478 lines
12 KiB
TypeScript

import React, {
createContext,
useContext,
useState,
useEffect,
useCallback,
} from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
import {
KeyboardCustomization,
KeyConfig,
KeyboardRow,
PresetType,
KeyboardSettings,
} from "@/types/keyboard";
import { getPresetById } from "@/app/tabs/sessions/terminal/keyboard/KeyDefinitions";
const STORAGE_KEY = "keyboardCustomization";
const DEFAULT_PRESET_ID: PresetType = "default";
const getDefaultConfig = (): KeyboardCustomization => {
const defaultPreset = getPresetById(DEFAULT_PRESET_ID);
if (!defaultPreset) {
throw new Error("Default preset not found");
}
return {
preset: DEFAULT_PRESET_ID,
version: 1,
topBar: {
pinnedKeys: [],
keys: [...defaultPreset.topBar.keys],
},
fullKeyboard: {
rows: defaultPreset.fullKeyboard.rows.map((row) => ({
...row,
keys: [...row.keys],
})),
},
settings: {
keySize: "medium",
compactMode: false,
hapticFeedback: false,
showHints: true,
},
};
};
interface KeyboardCustomizationContextType {
config: KeyboardCustomization;
isLoading: boolean;
setPreset: (presetId: PresetType) => Promise<void>;
addPinnedKey: (key: KeyConfig) => Promise<void>;
removePinnedKey: (keyId: string) => Promise<void>;
reorderPinnedKeys: (keys: KeyConfig[]) => Promise<void>;
addTopBarKey: (key: KeyConfig) => Promise<void>;
removeTopBarKey: (keyId: string) => Promise<void>;
reorderTopBarKeys: (keys: KeyConfig[]) => Promise<void>;
addRow: (row: KeyboardRow) => Promise<void>;
removeRow: (rowId: string) => Promise<void>;
reorderRows: (rows: KeyboardRow[]) => Promise<void>;
updateRow: (rowId: string, updates: Partial<KeyboardRow>) => Promise<void>;
toggleRowVisibility: (rowId: string) => Promise<void>;
addKeyToRow: (rowId: string, key: KeyConfig) => Promise<void>;
removeKeyFromRow: (rowId: string, keyId: string) => Promise<void>;
reorderKeysInRow: (rowId: string, keys: KeyConfig[]) => Promise<void>;
updateSettings: (settings: Partial<KeyboardSettings>) => Promise<void>;
resetToDefault: () => Promise<void>;
resetTopBar: () => Promise<void>;
resetFullKeyboard: () => Promise<void>;
exportConfig: () => string;
importConfig: (jsonString: string) => Promise<void>;
}
const KeyboardCustomizationContext = createContext<
KeyboardCustomizationContextType | undefined
>(undefined);
export const KeyboardCustomizationProvider: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const [config, setConfig] =
useState<KeyboardCustomization>(getDefaultConfig());
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadConfig = async () => {
try {
const stored = await AsyncStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored) as KeyboardCustomization;
setConfig(parsed);
}
} catch (error) {
console.error("Failed to load keyboard configuration:", error);
} finally {
setIsLoading(false);
}
};
loadConfig();
}, []);
const saveConfig = useCallback(async (newConfig: KeyboardCustomization) => {
try {
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newConfig));
setConfig(newConfig);
} catch (error) {
console.error("Failed to save keyboard configuration:", error);
}
}, []);
const setPreset = useCallback(
async (presetId: PresetType) => {
const preset = getPresetById(presetId);
if (!preset) {
console.error(`Preset ${presetId} not found`);
return;
}
const newConfig: KeyboardCustomization = {
preset: presetId,
version: 1,
topBar: {
pinnedKeys: [],
keys: [...preset.topBar.keys],
},
fullKeyboard: {
rows: preset.fullKeyboard.rows.map((row) => ({
...row,
keys: [...row.keys],
})),
},
settings: { ...config.settings },
};
await saveConfig(newConfig);
},
[config.settings, saveConfig],
);
const addPinnedKey = useCallback(
async (key: KeyConfig) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
topBar: {
...config.topBar,
pinnedKeys: [...config.topBar.pinnedKeys, key],
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const removePinnedKey = useCallback(
async (keyId: string) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
topBar: {
...config.topBar,
pinnedKeys: config.topBar.pinnedKeys.filter((k) => k.id !== keyId),
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const reorderPinnedKeys = useCallback(
async (keys: KeyConfig[]) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
topBar: {
...config.topBar,
pinnedKeys: keys,
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const addTopBarKey = useCallback(
async (key: KeyConfig) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
topBar: {
...config.topBar,
keys: [...config.topBar.keys, key],
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const removeTopBarKey = useCallback(
async (keyId: string) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
topBar: {
...config.topBar,
keys: config.topBar.keys.filter((k) => k.id !== keyId),
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const reorderTopBarKeys = useCallback(
async (keys: KeyConfig[]) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
topBar: {
...config.topBar,
keys,
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const addRow = useCallback(
async (row: KeyboardRow) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
fullKeyboard: {
rows: [...config.fullKeyboard.rows, row],
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const removeRow = useCallback(
async (rowId: string) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
fullKeyboard: {
rows: config.fullKeyboard.rows.filter((r) => r.id !== rowId),
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const reorderRows = useCallback(
async (rows: KeyboardRow[]) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
fullKeyboard: {
rows,
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const updateRow = useCallback(
async (rowId: string, updates: Partial<KeyboardRow>) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
fullKeyboard: {
rows: config.fullKeyboard.rows.map((row) =>
row.id === rowId ? { ...row, ...updates } : row,
),
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const toggleRowVisibility = useCallback(
async (rowId: string) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
fullKeyboard: {
rows: config.fullKeyboard.rows.map((row) =>
row.id === rowId ? { ...row, visible: !row.visible } : row,
),
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const addKeyToRow = useCallback(
async (rowId: string, key: KeyConfig) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
fullKeyboard: {
rows: config.fullKeyboard.rows.map((row) =>
row.id === rowId ? { ...row, keys: [...row.keys, key] } : row,
),
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const removeKeyFromRow = useCallback(
async (rowId: string, keyId: string) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
fullKeyboard: {
rows: config.fullKeyboard.rows.map((row) =>
row.id === rowId
? { ...row, keys: row.keys.filter((k) => k.id !== keyId) }
: row,
),
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const reorderKeysInRow = useCallback(
async (rowId: string, keys: KeyConfig[]) => {
const newConfig = {
...config,
preset: "custom" as PresetType,
fullKeyboard: {
rows: config.fullKeyboard.rows.map((row) =>
row.id === rowId ? { ...row, keys } : row,
),
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const updateSettings = useCallback(
async (settings: Partial<KeyboardSettings>) => {
const newConfig = {
...config,
settings: {
...config.settings,
...settings,
},
};
await saveConfig(newConfig);
},
[config, saveConfig],
);
const resetToDefault = useCallback(async () => {
await saveConfig(getDefaultConfig());
}, [saveConfig]);
const resetTopBar = useCallback(async () => {
const defaultPreset = getPresetById(
config.preset === "custom" ? DEFAULT_PRESET_ID : config.preset,
);
if (!defaultPreset) return;
const newConfig = {
...config,
topBar: {
pinnedKeys: [],
keys: [...defaultPreset.topBar.keys],
},
};
await saveConfig(newConfig);
}, [config, saveConfig]);
const resetFullKeyboard = useCallback(async () => {
const defaultPreset = getPresetById(
config.preset === "custom" ? DEFAULT_PRESET_ID : config.preset,
);
if (!defaultPreset) return;
const newConfig = {
...config,
fullKeyboard: {
rows: defaultPreset.fullKeyboard.rows.map((row) => ({
...row,
keys: [...row.keys],
})),
},
};
await saveConfig(newConfig);
}, [config, saveConfig]);
const exportConfig = useCallback(() => {
return JSON.stringify(config, null, 2);
}, [config]);
const importConfig = useCallback(
async (jsonString: string) => {
try {
const imported = JSON.parse(jsonString) as KeyboardCustomization;
if (!imported.topBar || !imported.fullKeyboard || !imported.settings) {
throw new Error("Invalid configuration structure");
}
await saveConfig(imported);
} catch (error) {
console.error("Failed to import configuration:", error);
throw error;
}
},
[saveConfig],
);
const value: KeyboardCustomizationContextType = {
config,
isLoading,
setPreset,
addPinnedKey,
removePinnedKey,
reorderPinnedKeys,
addTopBarKey,
removeTopBarKey,
reorderTopBarKeys,
addRow,
removeRow,
reorderRows,
updateRow,
toggleRowVisibility,
addKeyToRow,
removeKeyFromRow,
reorderKeysInRow,
updateSettings,
resetToDefault,
resetTopBar,
resetFullKeyboard,
exportConfig,
importConfig,
};
return (
<KeyboardCustomizationContext.Provider value={value}>
{children}
</KeyboardCustomizationContext.Provider>
);
};
export const useKeyboardCustomization = () => {
const context = useContext(KeyboardCustomizationContext);
if (!context) {
throw new Error(
"useKeyboardCustomization must be used within a KeyboardCustomizationProvider",
);
}
return context;
};