A flexible, tree-based menu resolver for building command-line interfaces with hierarchical navigation.
- Tree Structure: Define menus with arbitrary depth.
- Flat Indexing: Efficient O(1) lookup for menu nodes.
- Navigation: State management for traversing the menu tree with
goBack()support. - Action Resolution: Attach executable functions or data to menu items.
- Type-Safe: Full TypeScript support with exported types.
npm install menu-resolverimport TreeMenuResolver, { Menu } from "menu-resolver";
// Define a type for your menu data (optional but recommended)
type MenuData = { title: string; id?: string };
// 1. Define your menu structure
const menuStructure: Menu<MenuData>[] = [
{
data: { title: "Start Game" },
resolve: () => console.log("Starting game..."),
},
{
data: { title: "Settings" },
children: [
{
data: { title: "Audio" },
resolve: () => console.log("Opening audio settings..."),
},
{
data: { title: "Graphics" },
resolve: () => console.log("Opening graphics settings..."),
},
],
},
{
data: { title: "Exit" },
resolve: () => process.exit(0),
},
];
// 2. Initialize the resolver with options
// injectIdKey: "id" will automatically add the generated UUID to the 'id' field in MenuData
const resolver = new TreeMenuResolver(menuStructure, { injectIdKey: "id" });
// 3. Get the top-level menu items
const mainOptions = resolver.getDisplayableMenu();
console.log(mainOptions.map(o => o.title));
// Output: ['Start Game', 'Settings', 'Exit']
// 4. Navigate to a submenu (e.g., "Settings")
const settingsNode = mainOptions.find(o => o.title === "Settings");
if (settingsNode && settingsNode.id) {
const result = resolver.choose(settingsNode.id);
const settingsOptions = resolver.getDisplayableMenu();
console.log(settingsOptions.map(o => o.title));
// Output: ['Audio', 'Graphics']
// 5. Execute a resolve function if it exists
if (result.resolve) {
result.resolve();
}
}The resolve function receives a ResolverAPI object that provides navigation capabilities:
import TreeMenuResolver, { Menu, ResolverAPI } from "menu-resolver";
type MenuData = { title: string };
const menuStructure: Menu<MenuData>[] = [
{
data: { title: "User Management" },
children: [
{
data: { title: "Create User" },
resolve: (api: ResolverAPI<MenuData>) => {
console.log("Creating user...");
// After creating user, go back to main menu
api.goBack();
},
},
{
data: { title: "Delete User" },
resolve: (api: ResolverAPI<MenuData>) => {
console.log("Deleting user...");
// Navigate back after action
api.goBack();
},
},
{
data: { title: "Back to Main Menu" },
resolve: (api: ResolverAPI<MenuData>) => {
api.goBack();
},
},
],
},
{
data: { title: "Reports" },
children: [
{
data: { title: "Generate Report" },
resolve: (api: ResolverAPI<MenuData>) => {
console.log("Generating report...");
// Stay in the same menu level
},
},
],
},
];
const resolver = new TreeMenuResolver(menuStructure, { injectIdKey: 'id' });
// Navigate to User Management
const menu = resolver.getDisplayableMenu();
const userMgmt = menu.find(o => o.title === "User Management");
if (userMgmt && userMgmt.id) {
resolver.choose(userMgmt.id);
// Choose "Create User"
const subMenu = resolver.getDisplayableMenu();
const createUser = subMenu.find(o => o.title === "Create User");
if (createUser && createUser.id) {
const result = resolver.choose(createUser.id);
if (result.resolve) {
result.resolve(); // This will create user and go back automatically
}
}
}Initializes the menu resolver with a tree of menu items.
Parameters:
menu: Array ofMenuobjects defining the menu structure.options: Configuration options.injectIdKey: (Required) The key in your data object where the generated Node ID should be injected.
Returns the list of menu items for the current level. The items are flattened T objects, containing the injected ID if configured.
Returns:
T[]Example:
const options = resolver.getDisplayableMenu();
// [{ id: "uuid-1", title: "Start Game" }, ...]Selects a menu item by its ID and updates the current navigation level.
Parameters:
id: The unique identifier of the menu item to select.
Returns:
{ id: string; resolve?: (api: ResolverAPI<T>) => void | any }Behavior:
- If the item has children, the current level updates to show those children.
- If the item is a leaf node (no children), the navigation state remains unchanged.
- Returns an object containing the selected node's
idandresolvefunction (if defined).
Throws:
Errorif the ID is invalid or not found.
Example:
const result = resolver.choose(nodeId);
if (result.resolve) {
result.resolve(); // Execute the action
}Retrieves a complete node directly by its ID.
Parameters:
id: The unique identifier of the node.
Returns:
Node<T> | undefinedExample:
const node = resolver.findNodeById(nodeId);
if (node) {
console.log(node.data, node.parentKey);
}Navigates back to the parent node of the currently selected node.
Behavior:
- Updates the current level to show the parent's children (siblings of current node).
- Can navigate all the way back to the top level (main menu).
- When at top level,
currentNodeIdbecomesnull.
Throws:
"You haven't chosen any node"if no node is currently selected (already at top level)."Current node with id {id} not found"if the current node ID is invalid.
Example:
const topLevel = resolver.getDisplayableMenu();
const settingsNode = topLevel.find(o => o.title === "Settings");
resolver.choose(settingsNode.id);
// Now at Settings submenu
const settingsOptions = resolver.getDisplayableMenu();
// Go back to main menu
resolver.goBack();
const backToMain = resolver.getDisplayableMenu();
// Try to go back again from top level
resolver.goBack(); // Throws: "You haven't chosen any node"Defines the structure of a menu item. It is a union of three types:
A node that contains children but no resolve action.
type MenuParent<T> = {
data?: T;
children: Menu<T>[];
resolve?: undefined;
};A node with a string identifier for resolution.
type MenuActionSimple<T> = {
data?: T;
resolve: string;
children?: Menu<T>[];
};A node with a custom resolve function.
type MenuActionCustom<T> = {
data?: T;
resolve: (rsApi: ResolverAPI<T>) => any;
children?: Menu<T>[];
};Internal representation of a menu item with navigation metadata.
type Node<T> = {
id: string; // Unique identifier (auto-generated UUID)
data?: T; // Custom data
resolve?: () => void | any; // Wrapped resolve function
parentKey: string | null; // ID of parent node, or null for top-level
};API object passed to resolve functions for navigation control.
type ResolverAPI<T> = {
goBack: () => void; // Navigate back to the parent menu level
choose: (nodeId: string) => void; // Choose a node to navigate there.
currentNode: Node<T> | undefined; // Node selected
};Usage in resolve functions:
const menu: Menu<MyData> = {
data: { title: "Save and Exit" },
resolve: (api: ResolverAPI<MyData>) => {
saveData();
api.goBack(); // Return to previous menu after saving
},
};MIT