Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 3 additions & 40 deletions src/ui/tui/hooks/useKeyboardHints.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
* KeyboardHintsProvider — Context for collecting and displaying keyboard hints.
*
* Input components register their hints via useKeyBindings. The provider
* flattens, deduplicates, and sorts them. It auto-dismisses 3s after the
* first keypress and resets when the hint set changes (screen navigation).
* flattens, deduplicates, and sorts them. The hints bar stays visible for as
* long as a screen has registered hints — it never auto-dismisses.
*/

import { useInput } from 'ink';
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
type ReactNode,
Expand All @@ -28,29 +26,23 @@ interface KeyboardHintsContextValue {
register(id: string, hints: KeyboardHint[]): void;
unregister(id: string): void;
hints: KeyboardHint[];
visible: boolean;
}

const KeyboardHintsContext = createContext<KeyboardHintsContextValue>({
register: () => undefined,
unregister: () => undefined,
hints: [],
visible: false,
});

export const useKeyboardHintsContext = () => useContext(KeyboardHintsContext);

const DISMISS_DELAY = 3000;

export const KeyboardHintsProvider = ({
children,
}: {
children: ReactNode;
}) => {
const registrationsRef = useRef(new Map<string, KeyboardHint[]>());
const [hints, setHints] = useState<KeyboardHint[]>([]);
const [visible, setVisible] = useState(true);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const prevHintsKeyRef = useRef('');

const recompute = useCallback(() => {
Expand All @@ -64,14 +56,6 @@ export const KeyboardHintsProvider = ({
if (newKey !== prevHintsKeyRef.current) {
prevHintsKeyRef.current = newKey;
setHints(deduped);
// Reset visibility when hints change (new screen)
if (newKey.length > 0) {
setVisible(true);
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}
}
}, []);

Expand All @@ -91,29 +75,8 @@ export const KeyboardHintsProvider = ({
[recompute],
);

// Dismiss on first keypress after 3s
useInput(() => {
if (!visible) return;
if (timerRef.current) return; // already counting down
timerRef.current = setTimeout(() => {
setVisible(false);
timerRef.current = null;
}, DISMISS_DELAY);
});

// Cleanup timer on unmount
useEffect(() => {
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, []);

return (
<KeyboardHintsContext.Provider
value={{ register, unregister, hints, visible }}
>
<KeyboardHintsContext.Provider value={{ register, unregister, hints }}>
{children}
</KeyboardHintsContext.Provider>
);
Expand Down
4 changes: 2 additions & 2 deletions src/ui/tui/playground/demos/KeyboardHintsDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
*
* Cycles through SinglePicker, MultiPicker, GroupedPicker, and Confirmation
* so the user can see the hints bar update automatically for each component.
* The bar appears at the bottom of the screen and dismisses 3s after the
* first keypress, then reappears when the component changes.
* The bar appears at the bottom of the screen and stays visible, updating to
* match the active component.
*/

import { Box, Text } from 'ink';
Expand Down
32 changes: 14 additions & 18 deletions src/ui/tui/primitives/KeyboardHintsBar.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
/**
* KeyboardHintsBar — Row showing active keyboard shortcuts.
*
* Always reserves its row to prevent layout shift. When hints are
* visible, renders them in dimmed grey text. When dismissed, renders
* an empty reserved row.
* Always reserves its row to prevent layout shift, and always renders the
* active hints (in dimmed grey text) while a screen has registered them.
*/

import { Box, Text } from 'ink';
import { useKeyboardHintsContext } from '@ui/tui/hooks/useKeyboardHints';
import { Colors } from '@ui/tui/styles';

export const KeyboardHintsBar = () => {
const { hints, visible } = useKeyboardHintsContext();

const showHints = visible && hints.length > 0;
const { hints } = useKeyboardHintsContext();

return (
<Box height={1} paddingX={1}>
{showHints &&
hints.map((hint, i) => (
<Box
key={`${hint.label}-${hint.action}`}
marginRight={i < hints.length - 1 ? 2 : 0}
>
<Text bold color={Colors.muted}>
{hint.label}
</Text>
<Text dimColor> {hint.action}</Text>
</Box>
))}
{hints.map((hint, i) => (
<Box
key={`${hint.label}-${hint.action}`}
marginRight={i < hints.length - 1 ? 2 : 0}
>
<Text bold color={Colors.muted}>
{hint.label}
</Text>
<Text dimColor> {hint.action}</Text>
</Box>
))}
</Box>
);
};
Loading