Files
xo-cli/src/tui/App.tsx
2026-03-23 03:51:51 +00:00

183 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Main App component for the XO Wallet CLI.
* Uses Ink for terminal rendering with React components.
*/
import React from 'react';
import { Box, Text, useApp } from 'ink';
import { NavigationProvider, useNavigation } from './hooks/useNavigation.js';
import { AppProvider, useAppContext, useDialog, useStatus } from './hooks/useAppContext.js';
import { InputLayerProvider, useBlockableInput } from './hooks/useInputLayer.js';
import type { AppConfig } from '../app.js';
import { colors, logoSmall } from './theme.js';
// Screen imports
import { SeedInputScreen } from './screens/SeedInput.js';
import { WalletStateScreen } from './screens/WalletState.js';
import { TemplateListScreen } from './screens/TemplateList.js';
import { ActionWizardScreen } from './screens/action-wizard/ActionWizardScreen.js';
import { InvitationScreen } from './screens/invitations/InvitationScreen.js';
import { TransactionScreen } from './screens/Transaction.js';
import { MessageDialog } from './components/Dialog.js';
/**
* Props for the App component.
*/
interface AppProps {
config: AppConfig;
}
/**
* Router component that renders the current screen.
*/
function Router(): React.ReactElement {
const { screen } = useNavigation();
switch (screen) {
case 'seed-input':
return <SeedInputScreen />;
case 'wallet':
return <WalletStateScreen />;
case 'templates':
return <TemplateListScreen />;
case 'wizard':
return <ActionWizardScreen />;
case 'invitations':
return <InvitationScreen />;
case 'transaction':
return <TransactionScreen />;
default:
return <Text color={colors.error}>Unknown screen: {screen}</Text>;
}
}
/**
* Status bar component shown at the bottom of the screen.
*/
function StatusBar(): React.ReactElement {
const { status } = useStatus();
const { screen, canGoBack } = useNavigation();
return (
<Box
borderStyle="single"
borderColor={colors.border}
paddingX={1}
justifyContent="space-between"
>
<Text color={colors.primary} bold>{logoSmall}</Text>
<Text color={colors.textMuted}>{status}</Text>
<Text color={colors.textMuted}>
{canGoBack ? 'ESC: Back | ' : ''}q: Quit
</Text>
</Box>
);
}
/**
* Dialog overlay component for modals.
*/
function DialogOverlay(): React.ReactElement | null {
const { dialog } = useDialog();
if (!dialog?.visible) return null;
const borderColor = dialog.type === 'error' ? colors.error :
dialog.type === 'confirm' ? colors.warning :
colors.info;
return (
<Box
position="absolute"
flexDirection="column"
alignItems="center"
justifyContent="center"
width="100%"
height="100%"
>
<MessageDialog
title={dialog.type === 'error' ? '✗ Error' :
dialog.type === 'confirm' ? '? Confirm' :
' Info'}
message={dialog.message}
onClose={dialog.onCancel ?? (() => {})}
type={dialog.type as 'error' | 'info' | 'success'}
/>
</Box>
);
}
/**
* Main content wrapper with global keybindings.
*/
function MainContent(): React.ReactElement {
const { exit } = useApp();
const { goBack, canGoBack } = useNavigation();
const { screen } = useNavigation();
const appContext = useAppContext();
// Global keybindings — auto-blocked when any dialog/overlay is capturing input.
useBlockableInput((input, key) => {
// Quit on Ctrl+C
if (key.ctrl && input === 'c') {
appContext.exit();
exit();
}
// Go back on Escape
if (key.escape && canGoBack) {
goBack();
// If we went back to the seed input screen, remove the current engine
// TODO: This was to support going back to seed input then re-opening your seed, but there is a bug in the engine which prevents it from closing the current
// storage instance, giving us an error about the database already being opened.
if (screen === 'seed-input') {
appContext.appService?.engine.stop();
appContext.appService = null;
}
}
});
return (
<Box flexDirection="column" height="100%">
{/* Main content area */}
<Box flexDirection="column" flexGrow={1}>
<Router />
</Box>
{/* Status bar */}
<StatusBar />
{/* Dialog overlay */}
<DialogOverlay />
</Box>
);
}
/**
* Main App component.
* Sets up providers and renders the main content.
*/
export function App({ config }: AppProps): React.ReactElement {
const { exit } = useApp();
// Cleanup will be handled by React when components unmount
const handleExit = () => {
exit();
};
return (
<AppProvider
config={config}
onExit={handleExit}
>
<InputLayerProvider>
<NavigationProvider initialScreen="seed-input">
<MainContent />
</NavigationProvider>
</InputLayerProvider>
</AppProvider>
);
}