epiq 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/app.js +4 -5
  2. package/dist/board/components/Board.js +2 -2
  3. package/dist/board/components/CommandLine.js +11 -4
  4. package/dist/board/components/ContextBar.js +12 -3
  5. package/dist/board/components/Help.js +4 -4
  6. package/dist/board/components/Swimlane.d.ts +1 -0
  7. package/dist/board/components/Swimlane.js +3 -4
  8. package/dist/board/components/Swimlanes.js +8 -11
  9. package/dist/board/components/TicketListItem.js +3 -3
  10. package/dist/board/components/TicketUI.js +1 -1
  11. package/dist/board/hints/hints.d.ts +4 -4
  12. package/dist/board/hints/hints.js +7 -15
  13. package/dist/board/mock/board.js +500 -523
  14. package/dist/cli.d.ts +1 -1
  15. package/dist/cli.js +20 -61
  16. package/dist/debug-logger.d.ts +1 -1
  17. package/dist/debug-logger.js +5 -4
  18. package/dist/init-project.d.ts +1 -0
  19. package/dist/init-project.js +1 -0
  20. package/dist/navigation/actions/add-item/add-item-actions.js +21 -11
  21. package/dist/navigation/actions/board-action-map.js +3 -3
  22. package/dist/navigation/actions/default/default-action-utils.d.ts +8 -7
  23. package/dist/navigation/actions/default/default-action-utils.js +67 -17
  24. package/dist/navigation/actions/default/default-actions.js +30 -44
  25. package/dist/navigation/actions/default/navigation-action-utils.d.ts +16 -0
  26. package/dist/navigation/actions/default/navigation-action-utils.js +109 -0
  27. package/dist/navigation/actions/input/command-line-input.js +4 -4
  28. package/dist/navigation/actions/input/input-actions.js +13 -12
  29. package/dist/navigation/actions/move/move-actions-utils.d.ts +4 -5
  30. package/dist/navigation/actions/move/move-actions-utils.js +44 -31
  31. package/dist/navigation/actions/move/move-actions.js +17 -11
  32. package/dist/navigation/command-line/command-line-sequence-actions.d.ts +1 -1
  33. package/dist/navigation/command-line/command-line-sequence-actions.js +24 -11
  34. package/dist/navigation/command-line/command-line-sequence-intent.d.ts +3 -2
  35. package/dist/navigation/command-line/command-line-sequence-intent.js +13 -10
  36. package/dist/navigation/command-line/commands.d.ts +2 -0
  37. package/dist/navigation/command-line/commands.js +58 -0
  38. package/dist/navigation/keypress-listener.d.ts +1 -0
  39. package/dist/navigation/keypress-listener.js +33 -0
  40. package/dist/navigation/model/action-map.model.d.ts +4 -4
  41. package/dist/navigation/model/navigation-ctx.model.d.ts +1 -16
  42. package/dist/navigation/model/navigation-ctx.model.js +0 -1
  43. package/dist/navigation/model/navigation-tree.model.d.ts +2 -1
  44. package/dist/navigation/navigation.d.ts +3 -10
  45. package/dist/navigation/navigation.js +73 -79
  46. package/dist/navigation/state/command-line.state.d.ts +5 -0
  47. package/dist/navigation/state/command-line.state.js +21 -4
  48. package/dist/navigation/state/state.d.ts +17 -9
  49. package/dist/navigation/state/state.js +34 -35
  50. package/dist/navigation/utils/get-command-line-intent.d.ts +2 -2
  51. package/dist/navigation/utils/get-command-line-intent.js +7 -7
  52. package/dist/navigation/utils/key-intent.d.ts +5 -5
  53. package/dist/navigation/utils/key-intent.js +68 -74
  54. package/package.json +2 -2
  55. package/readme.md +54 -17
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  import './debug-logger.js';
3
- export declare let triggerRender: () => void;
3
+ export declare let renderBoard: () => void;
package/dist/cli.js CHANGED
@@ -1,80 +1,39 @@
1
1
  #!/usr/bin/env node
2
+ import './debug-logger.js';
2
3
  import { render } from 'ink';
3
4
  import meow from 'meow';
4
5
  import React from 'react';
5
6
  import App from './app.js';
6
- import { Hints } from './board/hints/hints.js';
7
+ import { initListeners } from './navigation/keypress-listener.js';
8
+ import { appState, initAppState } from './navigation/state/state.js';
7
9
  import { board } from './board/mock/board.js';
8
- import { ContextualActionMap } from './navigation/actions/board-action-map.js';
9
- import { inputActions } from './navigation/actions/input/input-actions.js';
10
- import { DefaultActions } from './navigation/actions/default/default-actions.js';
11
- import { navigate } from './navigation/navigation.js';
12
- import { patchState, updateState } from './navigation/state/state.js';
13
- import './debug-logger.js';
10
+ import { initProject } from './init-project.js';
14
11
  const cli = meow(`
15
- Usage
12
+ View board in directory:
16
13
  $ epiq
17
14
 
18
- Options
19
- --name Your name
20
-
21
- Examples
22
- $ epiq --name=Jane
23
- Hello, Jane
15
+ Init project in directory:
16
+ $ epiq --init "Project Name"
24
17
  `, {
25
18
  importMeta: import.meta,
26
19
  flags: {
27
- name: {
20
+ init: {
28
21
  type: 'string',
29
22
  },
30
23
  },
31
24
  });
32
25
  cli;
26
+ export let renderBoard = () => {
27
+ render(React.createElement(App, { board: appState.rootNode }));
28
+ };
29
+ process.stdout.on('resize', () => renderBoard());
33
30
  (() => {
34
- console.clear();
35
- const onBeforeRender = () => {
36
- updateState(state => {
37
- const { currentNode, mode } = state;
38
- const hints = (currentNode && Hints[currentNode.actionContext + mode]) ??
39
- (currentNode && Hints[currentNode.actionContext]) ?? [''];
40
- return {
41
- ...state,
42
- availableHints: hints,
43
- };
44
- });
45
- };
46
- navigate({
47
- index: 0,
48
- breadCrumb: [board],
49
- callbacks: {
50
- render: () => {
51
- onBeforeRender();
52
- render(React.createElement(App, { board: board }));
53
- },
54
- onSelectChange: (selected, breadCrumb) => {
55
- // container is the "list" we are navigating in (board → swimlanes, swimlane → tickets, etc)
56
- const container = breadCrumb.at(-1);
57
- // still remember which *child* is selected for rendering
58
- const currentNode = selected ?? container;
59
- // but build actions from the container type, not the selected child
60
- const containerType = container
61
- .actionContext;
62
- patchState({
63
- currentNode,
64
- breadCrumb,
65
- availableActions: [
66
- ...DefaultActions,
67
- ...ContextualActionMap[containerType],
68
- ...inputActions,
69
- ],
70
- });
71
- },
72
- },
73
- });
31
+ debug(cli);
32
+ if (cli.flags.init) {
33
+ initProject();
34
+ }
35
+ else if (!Object.keys(cli.flags).length) {
36
+ initAppState(board);
37
+ initListeners();
38
+ }
74
39
  })();
75
- export let triggerRender = () => {
76
- render(React.createElement(App, { board: board }));
77
- };
78
- process.stdout.on('resize', () => {
79
- render(React.createElement(App, { board: board }));
80
- });
@@ -1,2 +1,2 @@
1
- export declare function bug(...args: any[]): void;
1
+ export declare function debug(...args: any[]): void;
2
2
  export {};
@@ -2,14 +2,15 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import util from 'util';
4
4
  const DEFAULT_PATH = path.resolve(process.cwd(), 'debug.log');
5
- export function bug(...args) {
6
- const timestamp = new Date().toISOString();
5
+ export function debug(...args) {
6
+ // const timestamp = new Date().toISOString();
7
7
  const message = util.format(...args);
8
- const line = `[${timestamp}] ${message}\n`;
8
+ // const line = `[${timestamp}] ${message}\n`;
9
+ const line = `${message}\n`;
9
10
  // Ensure parent directory exists
10
11
  fs.mkdirSync(path.dirname(DEFAULT_PATH), { recursive: true });
11
12
  // appendFileSync creates the file if it does not exist
12
13
  fs.appendFileSync(DEFAULT_PATH, line, 'utf8');
13
14
  }
14
15
  // make it global
15
- globalThis.bug = bug;
16
+ globalThis.debug = debug;
@@ -0,0 +1 @@
1
+ export declare const initProject: () => void;
@@ -0,0 +1 @@
1
+ export const initProject = () => console.log('initProject');
@@ -1,35 +1,45 @@
1
1
  import { BoardItemTypes, } from '../../../board/model/board.model.js';
2
- export const addSwimlaneAction = async (ctx, _, { value }) => {
2
+ import { appState } from '../../state/state.js';
3
+ import { navigator } from '../default/navigation-action-utils.js';
4
+ export const addSwimlaneAction = async (_ctx, _cmd, { value }) => {
3
5
  const newItem = {
4
6
  id: `${Date.now()}`,
5
7
  name: value || 'New lane',
6
8
  description: '...',
7
9
  actionContext: BoardItemTypes.SWIMLANE,
8
- children: [],
9
10
  isSelected: false,
10
11
  childrenRenderAxis: 'vertical',
11
12
  enableChildNavigationAcrossContainers: true,
13
+ children: [],
12
14
  };
13
- const parent = ctx.navigationNode;
15
+ const parent = appState.currentNode;
14
16
  parent.children ?? (parent.children = []);
15
17
  const newItemIndex = parent.children.length;
16
18
  parent.children.push(newItem);
17
- ctx.updateSelection(newItemIndex);
19
+ navigator.navigate({ selectedIndex: newItemIndex });
18
20
  };
19
- export const addTicketAction = async (ctx, _, { value }) => {
21
+ export const addTicketAction = async (_ctx, _cmd, { value }) => {
20
22
  const newItem = {
21
- id: `asdf${Date.now()}`,
23
+ id: `${Date.now()}`,
22
24
  name: value || 'New issue',
23
25
  actionContext: BoardItemTypes.TICKET_LIST_ITEM,
24
- children: [],
25
26
  isSelected: false,
26
27
  childrenRenderAxis: 'vertical',
27
- enableChildNavigationAcrossContainers: true,
28
- description: '',
28
+ children: [
29
+ {
30
+ isSelected: false,
31
+ id: `${Date.now()}`,
32
+ name: 'Description',
33
+ description: '...add description',
34
+ actionContext: BoardItemTypes.TICKET,
35
+ childrenRenderAxis: 'vertical',
36
+ children: [],
37
+ },
38
+ ],
29
39
  };
30
- const parent = ctx.navigationNode;
40
+ const parent = appState.currentNode;
31
41
  parent.children ?? (parent.children = []);
32
42
  const newItemIndex = parent.children.length;
33
43
  parent.children.push(newItem);
34
- ctx.updateSelection(newItemIndex);
44
+ navigator.navigate({ selectedIndex: newItemIndex });
35
45
  };
@@ -1,12 +1,12 @@
1
1
  import { BoardItemTypes } from '../../board/model/board.model.js';
2
2
  import { moveAcrossParents, moveWithinParent, toggleMoveMode, } from './move/move-actions.js';
3
3
  export const ContextualActionMap = {
4
- [BoardItemTypes.BOARD]: [],
5
- [BoardItemTypes.SWIMLANE]: [...toggleMoveMode, ...moveWithinParent],
6
- [BoardItemTypes.TICKET_LIST_ITEM]: [
4
+ [BoardItemTypes.BOARD]: [...toggleMoveMode, ...moveWithinParent],
5
+ [BoardItemTypes.SWIMLANE]: [
7
6
  ...toggleMoveMode,
8
7
  ...moveWithinParent,
9
8
  ...moveAcrossParents,
10
9
  ],
10
+ [BoardItemTypes.TICKET_LIST_ITEM]: [],
11
11
  [BoardItemTypes.TICKET]: [],
12
12
  };
@@ -1,7 +1,8 @@
1
- import { NavigateCtx } from '../../model/navigation-ctx.model.js';
2
- export declare const navigateToNextItem: (ctx: NavigateCtx) => void;
3
- export declare const navigateToPreviousItem: (ctx: NavigateCtx) => void;
4
- export declare const navigateToNextContainer: (ctx: NavigateCtx) => void;
5
- export declare const navigateToPreviousContainer: (ctx: NavigateCtx) => void;
6
- export declare const enterChildNode: (ctx: NavigateCtx) => Promise<void>;
7
- export declare const exitToParentNode: (ctx: NavigateCtx) => void;
1
+ import { NavigateUtils } from '../../model/navigation-ctx.model.js';
2
+ export declare const navigationUtils: NavigateUtils;
3
+ export declare const navigateToNextItem: () => void;
4
+ export declare const navigateToPreviousItem: () => void;
5
+ export declare const navigateToNextContainer: (ctx: NavigateUtils) => void;
6
+ export declare const navigateToPreviousContainer: (ctx: NavigateUtils) => void;
7
+ export declare const enterChildNode: (ctx: NavigateUtils) => Promise<void>;
8
+ export declare const exitToParentNode: (ctx: NavigateUtils) => void;
@@ -1,17 +1,67 @@
1
- const navigateByOffset = (ctx, offset) => {
2
- const len = ctx.children.length;
1
+ import { appState, patchState, updateState } from '../../state/state.js';
2
+ export const navigationUtils = {
3
+ updateSelection: (idx) => {
4
+ updateState(oldState => {
5
+ const currentNode = oldState.breadCrumb.at(-1) ?? oldState.breadCrumb[0];
6
+ if (!currentNode)
7
+ return oldState;
8
+ currentNode.children?.forEach((child, i) => (child.isSelected = i === idx));
9
+ return {
10
+ ...oldState,
11
+ selectedIndex: idx,
12
+ currentNode: currentNode, // keep currentNode in sync
13
+ breadCrumb: [...oldState.breadCrumb], // keep reference fresh if you rely on it
14
+ };
15
+ });
16
+ },
17
+ selectNone() {
18
+ this.updateSelection(-1);
19
+ },
20
+ confirm(sel) {
21
+ console.log(sel);
22
+ },
23
+ exit() {
24
+ process.exit(0);
25
+ },
26
+ enterChildNode(childNode) {
27
+ patchState({
28
+ breadCrumb: [...appState.breadCrumb, childNode],
29
+ currentNode: childNode,
30
+ selectedIndex: 0,
31
+ });
32
+ this.updateSelection(0);
33
+ },
34
+ enterParentNode() {
35
+ const crumb = appState.breadCrumb;
36
+ if (crumb.length < 2)
37
+ return;
38
+ const newCrumb = crumb.slice(0, -1);
39
+ const parentLevelNode = newCrumb.at(-1);
40
+ // find old parent index inside grandparent
41
+ const grandParent = newCrumb.at(-2);
42
+ const parentIndex = grandParent?.children?.findIndex(x => x.id === parentLevelNode.id) ?? 0;
43
+ patchState({
44
+ breadCrumb: newCrumb,
45
+ currentNode: parentLevelNode,
46
+ selectedIndex: parentIndex,
47
+ });
48
+ this.updateSelection(parentIndex);
49
+ },
50
+ };
51
+ const navigateByOffset = (offset) => {
52
+ const len = appState.currentNode.children.length;
3
53
  if (len === 0)
4
54
  return;
5
- const newIndex = (ctx.getSelectedIndex() + offset + len) % len;
6
- ctx.updateSelection(newIndex);
55
+ const newIndex = (appState.selectedIndex + offset + len) % len;
56
+ navigationUtils.updateSelection(newIndex);
7
57
  };
8
- export const navigateToNextItem = (ctx) => navigateByOffset(ctx, 1);
9
- export const navigateToPreviousItem = (ctx) => navigateByOffset(ctx, -1);
58
+ export const navigateToNextItem = () => navigateByOffset(1);
59
+ export const navigateToPreviousItem = () => navigateByOffset(-1);
10
60
  const navigateToSiblingContainer = (ctx, direction) => {
11
- if (!ctx.navigationNode.enableChildNavigationAcrossContainers)
61
+ if (!appState.currentNode.enableChildNavigationAcrossContainers)
12
62
  return;
13
- const parent = ctx.breadCrumb.at(-1);
14
- const grandParent = ctx.breadCrumb.at(-2);
63
+ const parent = appState.breadCrumb.at(-1);
64
+ const grandParent = appState.breadCrumb.at(-2);
15
65
  if (!parent || !grandParent)
16
66
  return;
17
67
  const parentIndex = grandParent.children.findIndex(x => x.id === parent.id);
@@ -21,19 +71,19 @@ const navigateToSiblingContainer = (ctx, direction) => {
21
71
  const nextSibling = sliced.find(x => x.children.length);
22
72
  if (!nextSibling?.children?.length)
23
73
  return;
24
- const prevIndex = ctx.getSelectedIndex();
74
+ const prevIndex = appState.selectedIndex;
25
75
  const boundedIndex = Math.min(prevIndex, nextSibling.children.length - 1);
26
76
  ctx.selectNone();
27
- ctx.reInvokeNavigate(boundedIndex, [
28
- ...ctx.breadCrumb.slice(0, -1),
29
- nextSibling,
30
- ]);
77
+ patchState({
78
+ selectedIndex: boundedIndex,
79
+ breadCrumb: [...appState.breadCrumb.slice(0, -1), nextSibling],
80
+ });
31
81
  };
32
82
  export const navigateToNextContainer = (ctx) => navigateToSiblingContainer(ctx, 1);
33
83
  export const navigateToPreviousContainer = (ctx) => navigateToSiblingContainer(ctx, -1);
34
84
  export const enterChildNode = async (ctx) => {
35
- const currentNode = ctx.breadCrumb.at(-1);
36
- const focusNode = currentNode?.children[ctx.getSelectedIndex()];
85
+ const currentNode = appState.breadCrumb.at(-1);
86
+ const focusNode = currentNode?.children[appState.selectedIndex];
37
87
  if (!currentNode || !focusNode)
38
88
  return;
39
89
  if (!focusNode?.children?.length) {
@@ -45,7 +95,7 @@ export const enterChildNode = async (ctx) => {
45
95
  }
46
96
  };
47
97
  export const exitToParentNode = (ctx) => {
48
- if (ctx.breadCrumb.length >= 2) {
98
+ if (appState.breadCrumb.length >= 2) {
49
99
  ctx.enterParentNode();
50
100
  }
51
101
  };
@@ -1,78 +1,64 @@
1
+ import { CmdIntent } from '../../command-line/command-line-sequence-intent.js';
1
2
  import { Mode } from '../../model/action-map.model.js';
2
- import { patchState } from '../../state/state.js';
3
- import { KeyIntent } from '../../utils/key-intent.js';
4
- import { enterChildNode, exitToParentNode, navigateToNextContainer, navigateToNextItem, navigateToPreviousContainer, navigateToPreviousItem, } from './default-action-utils.js';
3
+ import { updateCommandLineInput } from '../../state/command-line.state.js';
4
+ import { appState, patchState } from '../../state/state.js';
5
+ import { Intent } from '../../utils/key-intent.js';
6
+ import { navigator } from './navigation-action-utils.js';
5
7
  export const DefaultActions = [
6
8
  {
7
- intent: KeyIntent.ToggleCommandLine,
9
+ intent: Intent.ToggleCommandLine,
8
10
  mode: Mode.DEFAULT,
9
11
  description: '[:] Toggle command line',
10
- action: () => patchState({
11
- mode: Mode.COMMAND_LINE,
12
- }),
12
+ action: () => patchState({ mode: Mode.COMMAND_LINE }),
13
13
  },
14
14
  {
15
- intent: KeyIntent.ToggleCommandLine,
15
+ intent: Intent.ToggleCommandLine,
16
16
  mode: Mode.COMMAND_LINE,
17
17
  description: '[ESC] Toggle command line',
18
- action: () => patchState({
19
- mode: Mode.DEFAULT,
20
- }),
18
+ action: () => patchState({ mode: Mode.DEFAULT }),
21
19
  },
22
- // {
23
- // intent: KeyIntent.ToggleHelp,
24
- // mode: Mode.DEFAULT,
25
- // description: '[F1] Toggle HELP menu',
26
- // action: () =>
27
- // setState(state => ({
28
- // ...state,
29
- // mode: Mode.HELP,
30
- // })),
31
- // },
32
- // {
33
- // intent: KeyIntent.ToggleHelp,
34
- // mode: Mode.HELP,
35
- // description: '[F1] Close HELP menu',
36
- // action: () =>
37
- // setState(state => ({
38
- // ...state,
39
- // mode: Mode.DEFAULT,
40
- // })),
41
- // },
42
20
  {
43
- intent: KeyIntent.Confirm,
21
+ intent: Intent.Confirm,
44
22
  mode: Mode.DEFAULT,
45
- description: '[ENTER/E] Confirm/Enter context',
46
- action: enterChildNode,
23
+ description: '[ENTER] Confirm/Enter context',
24
+ action: navigator.enterChildNode,
47
25
  },
48
26
  {
49
- intent: KeyIntent.Exit,
27
+ intent: Intent.Exit,
50
28
  mode: Mode.DEFAULT,
51
29
  description: '[ESC/Q] Exit application',
52
- action: exitToParentNode,
30
+ action: navigator.enterParentNode,
53
31
  },
54
32
  {
55
33
  mode: Mode.DEFAULT,
56
34
  description: '[ARROWS/HJKL] Navigate',
57
35
  },
58
36
  {
59
- intent: KeyIntent.NavPreviousItem,
37
+ intent: Intent.NavPreviousItem,
60
38
  mode: Mode.DEFAULT,
61
- action: navigateToPreviousItem,
39
+ action: navigator.navigateToPreviousItem,
62
40
  },
63
41
  {
64
- intent: KeyIntent.NavNextItem,
42
+ intent: Intent.NavNextItem,
65
43
  mode: Mode.DEFAULT,
66
- action: navigateToNextItem,
44
+ action: navigator.navigateToNextItem,
67
45
  },
68
46
  {
69
- intent: KeyIntent.NavToPreviousContainer,
47
+ intent: Intent.NavToPreviousContainer,
70
48
  mode: Mode.DEFAULT,
71
- action: navigateToPreviousContainer,
49
+ action: navigator.navigateToPreviousContainer,
72
50
  },
73
51
  {
74
- intent: KeyIntent.NavToNextContainer,
52
+ intent: Intent.NavToNextContainer,
75
53
  mode: Mode.DEFAULT,
76
- action: navigateToNextContainer,
54
+ action: navigator.navigateToNextContainer,
55
+ },
56
+ {
57
+ intent: Intent.Edit,
58
+ mode: Mode.DEFAULT,
59
+ action: () => {
60
+ patchState({ mode: Mode.COMMAND_LINE });
61
+ updateCommandLineInput(() => `${CmdIntent.Rename} ${appState.currentNode?.name}`);
62
+ },
77
63
  },
78
64
  ];
@@ -0,0 +1,16 @@
1
+ import { NavigationTree } from '../../model/navigation-tree.model.js';
2
+ export interface Navigator {
3
+ navigate({ currentNode, selectedIndex, }: {
4
+ currentNode?: NavigationTree<NavigationTree>;
5
+ selectedIndex: number;
6
+ }): void;
7
+ confirm(selected: NavigationTree<NavigationTree>): void;
8
+ exit(): void;
9
+ enterChildNode(): void;
10
+ enterParentNode(): void;
11
+ navigateToNextItem: () => void;
12
+ navigateToPreviousItem: () => void;
13
+ navigateToNextContainer: () => void;
14
+ navigateToPreviousContainer: () => void;
15
+ }
16
+ export declare const navigator: Navigator;
@@ -0,0 +1,109 @@
1
+ import { appState, patchState } from '../../state/state.js';
2
+ export const navigator = {
3
+ confirm(sel) {
4
+ console.log(sel);
5
+ },
6
+ exit() {
7
+ process.exit(0);
8
+ },
9
+ enterChildNode() {
10
+ const currentNode = appState.currentNode;
11
+ const focusNode = currentNode?.children[appState.selectedIndex];
12
+ if (!currentNode || !focusNode)
13
+ return;
14
+ navigator.navigate({
15
+ currentNode: focusNode,
16
+ selectedIndex: 0,
17
+ });
18
+ },
19
+ enterParentNode() {
20
+ const breadCrumb = appState.breadCrumb;
21
+ if (breadCrumb.length < 2)
22
+ return;
23
+ const prevNode = breadCrumb.at(-1);
24
+ if (!prevNode)
25
+ return;
26
+ const newCurrentNode = breadCrumb.at(-2);
27
+ const newIndex = newCurrentNode?.children?.findIndex(x => x.id === prevNode.id) ?? 0;
28
+ navigator.navigate({
29
+ currentNode: newCurrentNode,
30
+ selectedIndex: newIndex,
31
+ });
32
+ },
33
+ navigateToNextItem: () => navigateByOffset(1),
34
+ navigateToPreviousItem: () => navigateByOffset(-1),
35
+ navigateToNextContainer: () => navigateToSiblingContainer(1),
36
+ navigateToPreviousContainer: () => navigateToSiblingContainer(-1),
37
+ navigate: ({ currentNode, selectedIndex }) => {
38
+ if (!currentNode) {
39
+ currentNode = appState.currentNode;
40
+ }
41
+ const findBreadCrumb = (node, targetId, path = []) => {
42
+ const nextPath = [...path, node];
43
+ if (node.id === targetId)
44
+ return nextPath;
45
+ for (const child of node.children ?? []) {
46
+ const found = findBreadCrumb(child, targetId, nextPath);
47
+ if (found.length)
48
+ return found;
49
+ }
50
+ return [];
51
+ };
52
+ const setIsSelected = (node, targetId, selectedIndex) => {
53
+ // Clear selection for all children at this node
54
+ for (const child of node.children ?? []) {
55
+ child.isSelected = false;
56
+ setIsSelected(child, targetId, selectedIndex);
57
+ }
58
+ // If this is the target container, select its child at selectedIndex
59
+ if (node.id === targetId) {
60
+ const children = node.children;
61
+ const idx = Math.max(0, Math.min(selectedIndex, children.length - 1));
62
+ if (children[idx]) {
63
+ children[idx].isSelected = true;
64
+ }
65
+ }
66
+ };
67
+ const root = appState.rootNode;
68
+ const breadCrumb = findBreadCrumb(root, currentNode.id);
69
+ setIsSelected(root, currentNode.id, selectedIndex);
70
+ patchState({
71
+ breadCrumb,
72
+ currentNode,
73
+ selectedIndex,
74
+ });
75
+ },
76
+ };
77
+ const navigateByOffset = (offset) => {
78
+ const len = appState.currentNode.children.length;
79
+ if (len === 0)
80
+ return;
81
+ const newIndex = (appState.selectedIndex + offset + len) % len;
82
+ navigator.navigate({ selectedIndex: newIndex });
83
+ };
84
+ const navigateToSiblingContainer = (direction) => {
85
+ if (!appState.currentNode.enableChildNavigationAcrossContainers)
86
+ return;
87
+ const currentNode = appState.breadCrumb.at(-1);
88
+ const parentNode = appState.breadCrumb.at(-2);
89
+ if (!currentNode || !parentNode)
90
+ return;
91
+ const siblings = parentNode.children ?? [];
92
+ const currentNodeIndex = siblings.findIndex(x => x.id === currentNode.id);
93
+ if (currentNodeIndex < 0)
94
+ return;
95
+ // Look for the next sibling "container" that actually has children
96
+ const candidates = direction === -1
97
+ ? siblings.slice(0, currentNodeIndex).toReversed()
98
+ : siblings.slice(currentNodeIndex + 1);
99
+ const nextSibling = candidates.find(x => (x.children?.length ?? 0) > 0);
100
+ if (!nextSibling?.children?.length)
101
+ return;
102
+ // Keep the same child index if possible, otherwise clamp to last child
103
+ const prevIndex = appState.selectedIndex;
104
+ const boundedIndex = Math.min(prevIndex, nextSibling.children.length - 1);
105
+ navigator.navigate({
106
+ currentNode: nextSibling,
107
+ selectedIndex: boundedIndex,
108
+ });
109
+ };
@@ -1,5 +1,5 @@
1
- import { commandLineSequenceActions } from '../../command-line/command-line-sequence-actions.js';
2
- import { getCommandLineIntent } from '../../command-line/command-line-sequence-intent.js';
1
+ import { getCommandIntent } from '../../command-line/command-line-sequence-intent.js';
2
+ import { commands } from '../../command-line/commands.js';
3
3
  import { clearCommandLine, getCommandLineInput, updateCommandHistory, } from '../../state/command-line.state.js';
4
4
  export const onConfirmCommandLineSequenceInput = (...args) => {
5
5
  const commandSequence = getCommandLineInput();
@@ -9,8 +9,8 @@ export const onConfirmCommandLineSequenceInput = (...args) => {
9
9
  const value = rest.join(' ').trim();
10
10
  if (!command)
11
11
  return;
12
- const intent = getCommandLineIntent(command);
13
- const actionMeta = commandLineSequenceActions.find(x => x.intent === intent);
12
+ const intent = getCommandIntent(command);
13
+ const actionMeta = commands.find(x => x.intent === intent);
14
14
  actionMeta?.action?.(ctx, actionMeta, { command, value });
15
15
  updateCommandHistory();
16
16
  clearCommandLine();
@@ -1,42 +1,43 @@
1
1
  import { Mode } from '../../model/action-map.model.js';
2
2
  import { getNextCommand, getPrevCommand, updateCommandLineInput, } from '../../state/command-line.state.js';
3
3
  import { patchState } from '../../state/state.js';
4
- import { KeyIntent } from '../../utils/key-intent.js';
4
+ import { Intent } from '../../utils/key-intent.js';
5
5
  import { onConfirmCommandLineSequenceInput } from './command-line-input.js';
6
6
  export const inputActions = [
7
7
  {
8
- intent: KeyIntent.ToggleHelp,
8
+ intent: Intent.ViewHelp,
9
+ mode: Mode.DEFAULT,
10
+ action: () => patchState({ mode: Mode.HELP }),
11
+ },
12
+ {
13
+ intent: Intent.Exit,
9
14
  mode: Mode.HELP,
10
- action: () => {
11
- patchState({
12
- mode: Mode.DEFAULT,
13
- });
14
- },
15
+ action: () => patchState({ mode: Mode.DEFAULT }),
15
16
  },
16
17
  {
17
- intent: KeyIntent.Confirm,
18
+ intent: Intent.Confirm,
18
19
  mode: Mode.COMMAND_LINE,
19
20
  action: (...args) => {
20
21
  onConfirmCommandLineSequenceInput(...args);
21
22
  },
22
23
  },
23
24
  {
24
- intent: KeyIntent.CaptureInput,
25
+ intent: Intent.CaptureInput,
25
26
  mode: Mode.COMMAND_LINE,
26
27
  action: (_1, _2, { sequence }) => updateCommandLineInput(s => s + sequence),
27
28
  },
28
29
  {
29
- intent: KeyIntent.EraseInput,
30
+ intent: Intent.EraseInput,
30
31
  mode: Mode.COMMAND_LINE,
31
32
  action: () => updateCommandLineInput(s => s.slice(0, -1)),
32
33
  },
33
34
  {
34
- intent: KeyIntent.GetLastCommandFromHistory,
35
+ intent: Intent.GetLastCommandFromHistory,
35
36
  mode: Mode.COMMAND_LINE,
36
37
  action: () => getPrevCommand(),
37
38
  },
38
39
  {
39
- intent: KeyIntent.GetNextCommandFromHistory,
40
+ intent: Intent.GetNextCommandFromHistory,
40
41
  mode: Mode.COMMAND_LINE,
41
42
  action: () => getNextCommand(),
42
43
  },