@mak-98/dirvi-cli 0.3.2 → 0.5.0

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 (90) hide show
  1. package/dist/application/effect-to-action.d.ts +4 -3
  2. package/dist/application/effect-to-action.d.ts.map +1 -1
  3. package/dist/application/effect-to-action.js +75 -71
  4. package/dist/application/reducer-action.d.ts +15 -11
  5. package/dist/application/reducer-action.d.ts.map +1 -1
  6. package/dist/application/reducer.d.ts +3 -2
  7. package/dist/application/reducer.d.ts.map +1 -1
  8. package/dist/application/reducer.js +66 -29
  9. package/dist/domain/app-api.d.ts +47 -0
  10. package/dist/domain/app-api.d.ts.map +1 -0
  11. package/dist/domain/app-api.js +21 -0
  12. package/dist/domain/event-message.d.ts +6 -6
  13. package/dist/domain/event-message.d.ts.map +1 -1
  14. package/dist/index.js +13 -10
  15. package/dist/infrastructure/get-dir-entries.d.ts +3 -0
  16. package/dist/infrastructure/get-dir-entries.d.ts.map +1 -0
  17. package/dist/infrastructure/get-dir-entries.js +33 -0
  18. package/dist/infrastructure/get-dir-lazy-entries.d.ts +3 -0
  19. package/dist/infrastructure/get-dir-lazy-entries.d.ts.map +1 -0
  20. package/dist/infrastructure/get-dir-lazy-entries.js +33 -0
  21. package/dist/infrastructure/get-unix-abs-path.d.ts +3 -0
  22. package/dist/infrastructure/get-unix-abs-path.d.ts.map +1 -0
  23. package/dist/infrastructure/get-unix-abs-path.js +14 -0
  24. package/dist/infrastructure/ink-input-to-user-input.d.ts +4 -0
  25. package/dist/infrastructure/ink-input-to-user-input.d.ts.map +1 -0
  26. package/dist/infrastructure/{user-input.js → ink-input-to-user-input.js} +10 -0
  27. package/dist/infrastructure/load-posix-app-api.d.ts +4 -0
  28. package/dist/infrastructure/load-posix-app-api.d.ts.map +1 -0
  29. package/dist/infrastructure/load-posix-app-api.js +82 -0
  30. package/dist/ui/App.d.ts +14 -8
  31. package/dist/ui/App.d.ts.map +1 -1
  32. package/dist/ui/App.js +68 -31
  33. package/dist/ui/AppShell.d.ts +4 -9
  34. package/dist/ui/AppShell.d.ts.map +1 -1
  35. package/dist/ui/AppShell.js +21 -7
  36. package/dist/ui/LoadingApp.d.ts +11 -0
  37. package/dist/ui/LoadingApp.d.ts.map +1 -0
  38. package/dist/ui/LoadingApp.js +67 -0
  39. package/dist/ui/components/StatusBar.d.ts +8 -0
  40. package/dist/ui/components/StatusBar.d.ts.map +1 -0
  41. package/dist/ui/components/StatusBar.js +7 -0
  42. package/dist/ui/components/ViewRowComponent.js +2 -2
  43. package/dist/ui/hooks/useView.d.ts +3 -2
  44. package/dist/ui/hooks/useView.d.ts.map +1 -1
  45. package/dist/ui/hooks/useView.js +5 -6
  46. package/dist/ui/view.d.ts +5 -4
  47. package/dist/ui/view.d.ts.map +1 -1
  48. package/dist/ui/view.js +55 -31
  49. package/package.json +3 -2
  50. package/dist/application/action.d.ts +0 -19
  51. package/dist/application/action.d.ts.map +0 -1
  52. package/dist/application/action.js +0 -1
  53. package/dist/application/dir-buffer-helpers.d.ts +0 -3
  54. package/dist/application/dir-buffer-helpers.d.ts.map +0 -1
  55. package/dist/application/dir-buffer-helpers.js +0 -21
  56. package/dist/application/display-rows.d.ts +0 -4
  57. package/dist/application/display-rows.d.ts.map +0 -1
  58. package/dist/application/display-rows.js +0 -50
  59. package/dist/application/fold-helpers.d.ts +0 -8
  60. package/dist/application/fold-helpers.d.ts.map +0 -1
  61. package/dist/application/fold-helpers.js +0 -43
  62. package/dist/application/intent-to-effect.d.ts +0 -39
  63. package/dist/application/intent-to-effect.d.ts.map +0 -1
  64. package/dist/application/intent-to-effect.js +0 -118
  65. package/dist/application/user-input-to-intent.d.ts +0 -15
  66. package/dist/application/user-input-to-intent.d.ts.map +0 -1
  67. package/dist/application/user-input-to-intent.js +0 -56
  68. package/dist/domain/print-message.d.ts +0 -9
  69. package/dist/domain/print-message.d.ts.map +0 -1
  70. package/dist/domain/print-message.js +0 -1
  71. package/dist/infrastructure/input.d.ts +0 -7
  72. package/dist/infrastructure/input.d.ts.map +0 -1
  73. package/dist/infrastructure/input.js +0 -81
  74. package/dist/infrastructure/load-initial-props.d.ts +0 -3
  75. package/dist/infrastructure/load-initial-props.d.ts.map +0 -1
  76. package/dist/infrastructure/load-initial-props.js +0 -29
  77. package/dist/infrastructure/user-input.d.ts +0 -17
  78. package/dist/infrastructure/user-input.d.ts.map +0 -1
  79. package/dist/ui/App2.d.ts +0 -11
  80. package/dist/ui/App2.d.ts.map +0 -1
  81. package/dist/ui/App2.js +0 -96
  82. package/dist/ui/components/DirEntries.d.ts +0 -8
  83. package/dist/ui/components/DirEntries.d.ts.map +0 -1
  84. package/dist/ui/components/DirEntries.js +0 -39
  85. package/dist/ui/components/UnixEntry.d.ts +0 -6
  86. package/dist/ui/components/UnixEntry.d.ts.map +0 -1
  87. package/dist/ui/components/UnixEntry.js +0 -12
  88. package/dist/ui/components/UnixEntryComponent.d.ts +0 -6
  89. package/dist/ui/components/UnixEntryComponent.d.ts.map +0 -1
  90. package/dist/ui/components/UnixEntryComponent.js +0 -12
@@ -0,0 +1,82 @@
1
+ import { join } from 'node:path';
2
+ import { watch } from 'node:fs';
3
+ import { createAppApis } from '../domain/app-api.js';
4
+ import { getUnixAbsPath } from './get-unix-abs-path.js';
5
+ import { getDirEntries } from './get-dir-entries.js';
6
+ export function loadPosixAppApi(directory) {
7
+ const unixAbsPath = getUnixAbsPath(directory ?? process.cwd());
8
+ const apis = createAppApis();
9
+ const subscribeToResync = createFsResyncSubscription(join(unixAbsPath));
10
+ return {
11
+ name: `Posix(${unixAbsPath})`,
12
+ emptyForestMessage: 'The directory is empty.',
13
+ loadBranches: (path) => {
14
+ const address = join(unixAbsPath, ...path);
15
+ return getDirEntries(address);
16
+ },
17
+ subscribeToResync,
18
+ ...apis,
19
+ };
20
+ }
21
+ // `recursive: true` is supported by macOS and Windows, but
22
+ // recursive watching is not supported on all Node/POSIX
23
+ // platforms: Linux unsupported.
24
+ function createFsResyncSubscription(directory) {
25
+ const listeners = new Set();
26
+ let watcher;
27
+ let notificationTimer;
28
+ function notifyListeners() {
29
+ for (const listener of listeners) {
30
+ listener();
31
+ }
32
+ }
33
+ function scheduleNotification() {
34
+ // fs.watch can emit several events for one filesystem operation.
35
+ // Debounce them into a single resync request.
36
+ if (notificationTimer !== undefined) {
37
+ clearTimeout(notificationTimer);
38
+ }
39
+ notificationTimer = setTimeout(() => {
40
+ notificationTimer = undefined;
41
+ if (listeners.size > 0) {
42
+ notifyListeners();
43
+ }
44
+ }, 100);
45
+ }
46
+ function startWatcher() {
47
+ if (watcher !== undefined) {
48
+ return;
49
+ }
50
+ watcher = watch(directory, {
51
+ recursive: true,
52
+ }, (_eventType, _filename) => {
53
+ scheduleNotification();
54
+ });
55
+ watcher.on('error', () => {
56
+ // The next loadBranches call will report the relevant error.
57
+ });
58
+ }
59
+ function stopWatcher() {
60
+ if (notificationTimer !== undefined) {
61
+ clearTimeout(notificationTimer);
62
+ notificationTimer = undefined;
63
+ }
64
+ watcher?.close();
65
+ watcher = undefined;
66
+ }
67
+ return (listener) => {
68
+ listeners.add(listener);
69
+ startWatcher();
70
+ let subscribed = true;
71
+ return () => {
72
+ if (!subscribed) {
73
+ return;
74
+ }
75
+ subscribed = false;
76
+ listeners.delete(listener);
77
+ if (listeners.size === 0) {
78
+ stopWatcher();
79
+ }
80
+ };
81
+ };
82
+ }
package/dist/ui/App.d.ts CHANGED
@@ -1,11 +1,17 @@
1
- import { State } from 'dirvi-lib';
2
- import type { UnixAbsolutePath } from 'dirvi-lib';
3
- import { EventMessage } from '../domain/event-message.js';
4
- export type AppProps = {
5
- cwdAddress: UnixAbsolutePath;
6
- initialState: State;
7
- print?: (message: EventMessage) => void;
1
+ import { IntentToEffect, SerializableKey, State, TreeNode } from 'dirvi-lib';
2
+ import type { EventMessage } from '../domain/event-message.js';
3
+ import { EffectToAction } from '../application/effect-to-action.js';
4
+ import { AppApi } from '../domain/app-api.js';
5
+ import { Reducer } from '../application/reducer.js';
6
+ type AppProps<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = {
7
+ appApi: AppApi<Id, BufferNode>;
8
+ initialState: State<Id, BufferNode>;
9
+ reducer: Reducer<Id, BufferNode>;
10
+ intentToEffect: IntentToEffect<Id, BufferNode>;
11
+ effectToAction: EffectToAction<Id, BufferNode>;
12
+ print?: (message: EventMessage<Id, BufferNode>) => void;
8
13
  onError?: (error: Error) => void;
9
14
  };
10
- export declare function App({ cwdAddress, initialState, print, onError }: AppProps): import("react").JSX.Element | null;
15
+ export declare function App<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>({ appApi, initialState, reducer, intentToEffect, effectToAction, print, onError, }: AppProps<Id, BufferNode>): import("react").JSX.Element | null;
16
+ export {};
11
17
  //# sourceMappingURL=App.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/ui/App.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAqC,KAAK,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAIlD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAO1D,MAAM,MAAM,QAAQ,GAAG;IACrB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,YAAY,EAAE,KAAK,CAAC;IACpB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,wBAAgB,GAAG,CAAC,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,QAAQ,sCAwHzE"}
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/ui/App.tsx"],"names":[],"mappings":"AAGA,OAAO,EAGL,cAAc,EAAE,eAAe,EAC/B,KAAK,EACL,QAAQ,EAET,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAGpE,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAGpD,KAAK,QAAQ,CACX,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,IACzC;IACF,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAC/B,YAAY,EAAE,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IACpC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IACjC,cAAc,EAAE,cAAc,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAC/C,cAAc,EAAE,cAAc,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAC/C,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE,EAAE,UAAU,CAAC,KAAK,IAAI,CAAC;IACxD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,wBAAgB,GAAG,CACjB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,EACA,MAAM,EACN,YAAY,EACZ,OAAO,EACP,cAAc,EACd,cAAc,EACd,KAAK,EACL,OAAO,GACR,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,sCAoL1B"}
package/dist/ui/App.js CHANGED
@@ -1,48 +1,86 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { join } from 'node:path';
3
- import { Box, Text, useApp, useInput } from 'ink';
4
- import { useEffect, useMemo, useReducer, useState } from 'react';
5
- import { getDirLazyEntries, NavigationNode } from 'dirvi-lib';
6
- import { useView } from './hooks/useView.js';
7
- import { reducer } from '../application/reducer.js';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useApp, useInput, useWindowSize } from 'ink';
3
+ import { useEffect, useMemo, useReducer, useRef, useState } from 'react';
4
+ import { userInputToIntent, } from 'dirvi-lib';
8
5
  import { ViewRowComponent } from './components/ViewRowComponent.js';
9
- import { inkInputToUserInput } from '../infrastructure/user-input.js';
10
- import { userInputToIntent } from '../application/user-input-to-intent.js';
11
- import { intentToEffect } from '../application/intent-to-effect.js';
12
- import { effectToAction } from '../application/effect-to-action.js';
13
- export function App({ cwdAddress, initialState, print, onError }) {
6
+ import { StatusBar } from './components/StatusBar.js';
7
+ import { inkInputToUserInput } from '../infrastructure/ink-input-to-user-input.js';
8
+ import { useView } from './hooks/useView.js';
9
+ export function App({ appApi, initialState, reducer, intentToEffect, effectToAction, print, onError, }) {
14
10
  const [state, dispatch] = useReducer(reducer, initialState);
15
- const navigation = useMemo(() => NavigationNode.from(state.buffer, state.folds), [state.buffer, state.folds]);
16
- const [commandBuffer, setCommandBuffer] = useState('');
17
- const view = useView(navigation, state);
11
+ // Give `subscribeToResync` callback a way to see current state:
12
+ const stateRef = useRef(state);
13
+ stateRef.current = state;
14
+ const navigation = useMemo(() => appApi.navNodeApi.from(state.buffer, state.foldNode), [appApi.navNodeApi, state.buffer, state.foldNode]);
15
+ const [inputState, setInputState] = useState({
16
+ inputMode: 'normal',
17
+ normalBuffer: '',
18
+ });
19
+ const { rows: terminalRows } = useWindowSize();
20
+ const view = useView(navigation, state, appApi.cursorApi, terminalRows);
18
21
  const [exitStatus, setExitStatus] = useState();
19
- // Print view on changes
22
+ // Print on changes: view, paths of visible leaves
20
23
  useEffect(() => {
21
24
  print?.({ type: 'view', view: state });
22
- const visibleFilesPaths = NavigationNode.visibleFilesPaths(navigation).map((path) => join(...path));
25
+ const visibleLeavesPaths = appApi.navNodeApi.visibleLeavesPaths(navigation);
23
26
  print?.({
24
- type: 'displayed-files-paths',
25
- paths: visibleFilesPaths,
27
+ type: 'displayed-leaves-paths',
28
+ paths: visibleLeavesPaths,
29
+ });
30
+ }, [appApi.navNodeApi, state, print, navigation]);
31
+ // Subscribe to directory watcher, do not
32
+ // resubscribe on every state change.
33
+ useEffect(() => {
34
+ let active = true;
35
+ const unsubscribe = appApi.subscribeToResync(() => {
36
+ const oldState = stateRef.current;
37
+ void appApi.stateApi
38
+ .resync(oldState, appApi.loadBranches)
39
+ .then((newState) => {
40
+ if (!active) {
41
+ return;
42
+ }
43
+ dispatch({
44
+ kind: 'setState',
45
+ oldState: oldState,
46
+ newState: newState,
47
+ });
48
+ })
49
+ .catch((error) => {
50
+ const appError = error instanceof Error ? error : new Error(String(error));
51
+ onError?.(appError);
52
+ });
26
53
  });
27
- }, [state, print, navigation, cwdAddress]);
54
+ return () => {
55
+ active = false;
56
+ unsubscribe();
57
+ };
58
+ }, [appApi, onError]);
28
59
  function executeEffect(effect) {
29
60
  if (effect === undefined) {
30
61
  return;
31
62
  }
32
63
  switch (effect.effectType) {
33
- case 'dispatchEffectAsAction':
64
+ case 'dispatchEffectAction':
34
65
  const action = effectToAction(effect.action, navigation, state);
35
66
  if (action === undefined) {
36
67
  return;
37
68
  }
38
69
  dispatch(action);
70
+ setInputState({
71
+ inputMode: 'normal',
72
+ normalBuffer: '',
73
+ });
74
+ return;
75
+ case 'setInputState':
76
+ setInputState(effect.inputState);
39
77
  return;
40
- case 'loadDir': {
41
- const address = join(cwdAddress, ...effect.path);
42
- void getDirLazyEntries(address)
78
+ case 'loadBranchEntries': {
79
+ void appApi
80
+ .loadBranches(effect.path)
43
81
  .then((entries) => {
44
82
  dispatch({
45
- kind: 'updateDir',
83
+ kind: 'updateBranch',
46
84
  path: effect.path,
47
85
  entries,
48
86
  });
@@ -54,13 +92,13 @@ export function App({ cwdAddress, initialState, print, onError }) {
54
92
  });
55
93
  return;
56
94
  }
57
- case 'printFile':
95
+ case 'emitPath':
58
96
  print?.({
59
97
  type: 'file',
60
98
  path: effect.path,
61
99
  });
62
100
  return;
63
- case 'exit':
101
+ case 'quit':
64
102
  setExitStatus(effect.exitMessage);
65
103
  return;
66
104
  }
@@ -71,7 +109,7 @@ export function App({ cwdAddress, initialState, print, onError }) {
71
109
  if (userInput === undefined) {
72
110
  return;
73
111
  }
74
- const intent = userInputToIntent(userInput, commandBuffer);
112
+ const intent = userInputToIntent(userInput, inputState);
75
113
  if (intent === undefined) {
76
114
  return;
77
115
  }
@@ -79,8 +117,7 @@ export function App({ cwdAddress, initialState, print, onError }) {
79
117
  if (effectResult === undefined) {
80
118
  return;
81
119
  }
82
- setCommandBuffer(effectResult.commandBuffer);
83
- executeEffect(effectResult.effect);
120
+ executeEffect(effectResult);
84
121
  });
85
122
  // --- Exit Logic ---
86
123
  const { exit } = useApp();
@@ -97,5 +134,5 @@ export function App({ cwdAddress, initialState, print, onError }) {
97
134
  return null;
98
135
  }
99
136
  // --- JSX ---
100
- return (_jsx(Box, { flexDirection: "column", children: view.rows.length === 0 ? (_jsx(Text, { dimColor: true, children: "Directory is empty." })) : (view.rows.map((row) => (_jsx(ViewRowComponent, { row: row }, row.id)))) }));
137
+ return (_jsxs(Box, { flexDirection: "column", height: terminalRows, children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: view.rows.length === 0 ? (_jsx(Text, { dimColor: true, children: "Empty." })) : (view.rows.map((row) => _jsx(ViewRowComponent, { row: row }, row.id))) }), _jsx(StatusBar, { inputState: inputState })] }));
101
138
  }
@@ -1,13 +1,8 @@
1
- import { UnixAbsolutePath, State } from 'dirvi-lib';
2
- import { EventMessage } from '../domain/event-message.js';
3
- export type EmptyView = {
4
- cursor: undefined;
5
- };
1
+ import { SerializableKey, TreeNode } from 'dirvi-lib';
6
2
  export type ShellAppProps = {
7
- cwdAddress: UnixAbsolutePath;
8
- initialState: State | EmptyView;
9
- print?: (message: EventMessage) => void;
3
+ directory?: string;
4
+ print?: (message: string) => void;
10
5
  onError?: (error: Error) => void;
11
6
  };
12
- export declare function AppShell({ cwdAddress, initialState, print, onError, }: ShellAppProps): import("react").JSX.Element;
7
+ export declare function AppShell<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>({ directory, print, onError }: ShellAppProps): import("react").JSX.Element;
13
8
  //# sourceMappingURL=AppShell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AppShell.d.ts","sourceRoot":"","sources":["../../src/ui/AppShell.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAI1D,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,gBAAgB,CAAC;IAC7B,YAAY,EAAE,KAAK,GAAG,SAAS,CAAC;IAChC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,wBAAgB,QAAQ,CAAC,EACvB,UAAU,EACV,YAAY,EACZ,KAAK,EACL,OAAO,GACR,EAAE,aAAa,+BAaf"}
1
+ {"version":3,"file":"AppShell.d.ts","sourceRoot":"","sources":["../../src/ui/AppShell.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAwB,eAAe,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAI5E,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAIF,wBAAgB,QAAQ,CACtB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,aAAa,+BAyB7C"}
@@ -1,9 +1,23 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Text } from 'ink';
3
- import { App } from './App.js';
4
- export function AppShell({ cwdAddress, initialState, print, onError, }) {
5
- if (initialState.cursor === undefined) {
6
- return _jsx(Text, { dimColor: true, children: "Directory is empty." });
7
- }
8
- return (_jsx(App, { cwdAddress: cwdAddress, initialState: initialState, print: print, onError: onError }));
2
+ import { loadPosixAppApi } from '../infrastructure/load-posix-app-api.js';
3
+ import { LoadingApp } from './LoadingApp.js';
4
+ import { join } from 'node:path';
5
+ import { useMemo } from 'react';
6
+ // App Shell is for choosing the active app, then loading the api,
7
+ // and showing the app.
8
+ export function AppShell({ directory, print, onError }) {
9
+ // Api owns directory watcher and subscriptions
10
+ const posixAppApi = useMemo(() => loadPosixAppApi(directory), [directory]);
11
+ const emitEventMsg = print
12
+ ? (message) => {
13
+ const externalMessage = message.type === 'displayed-leaves-paths'
14
+ ? {
15
+ ...message,
16
+ paths: message.paths.map((path) => join(...path)),
17
+ }
18
+ : message;
19
+ print(`${JSON.stringify(externalMessage)}\n`);
20
+ }
21
+ : undefined;
22
+ return (_jsx(LoadingApp, { appApi: posixAppApi, ...(emitEventMsg === undefined ? {} : { print: emitEventMsg }), ...(onError === undefined ? {} : { onError }) }));
9
23
  }
@@ -0,0 +1,11 @@
1
+ import { SerializableKey, TreeNode } from 'dirvi-lib';
2
+ import type { EventMessage } from '../domain/event-message.js';
3
+ import { AppApi } from '../domain/app-api.js';
4
+ type LoadingAppProps<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = {
5
+ appApi: AppApi<Id, BufferNode>;
6
+ print?: (message: EventMessage<Id, BufferNode>) => void;
7
+ onError?: (error: Error) => void;
8
+ };
9
+ export declare function LoadingApp<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>({ appApi, print, onError }: LoadingAppProps<Id, BufferNode>): import("react").JSX.Element;
10
+ export {};
11
+ //# sourceMappingURL=LoadingApp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LoadingApp.d.ts","sourceRoot":"","sources":["../../src/ui/LoadingApp.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAwB,eAAe,EAAS,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEnF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAI9C,KAAK,eAAe,CAClB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,IACzC;IACF,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE,EAAE,UAAU,CAAC,KAAK,IAAI,CAAC;IACxD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AA0BF,wBAAgB,UAAU,CACxB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,eAAe,CAAC,EAAE,EAAE,UAAU,CAAC,+BA+F5D"}
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { createIntentToEffect } from 'dirvi-lib';
5
+ import { App } from './App.js';
6
+ import { createReducer } from '../application/reducer.js';
7
+ import { createEffectToAction } from '../application/effect-to-action.js';
8
+ function createInitialState(rootBranches, createEmptyFoldRoot) {
9
+ if (rootBranches.length === 0) {
10
+ throw new Error('createInitialState cannot create a state for an empty forest');
11
+ }
12
+ return {
13
+ buffer: rootBranches,
14
+ foldNode: createEmptyFoldRoot(),
15
+ cursor: {
16
+ kind: 'entry',
17
+ parentPath: [],
18
+ entryId: rootBranches[0].id,
19
+ },
20
+ };
21
+ }
22
+ export function LoadingApp({ appApi, print, onError }) {
23
+ const [initialState, setInitialState] = useState();
24
+ const [empty, setEmpty] = useState(false);
25
+ const [error, setError] = useState();
26
+ useEffect(() => {
27
+ let mounted = true;
28
+ appApi
29
+ .loadBranches([])
30
+ .then((rootBranches) => {
31
+ if (!mounted) {
32
+ return;
33
+ }
34
+ if (rootBranches.length === 0) {
35
+ setEmpty(true);
36
+ return;
37
+ }
38
+ setInitialState(createInitialState(rootBranches, () => appApi.foldNodeService.createEmptyRoot()));
39
+ })
40
+ .catch((cause) => {
41
+ const nextError = cause instanceof Error ? cause : new Error(String(cause));
42
+ if (!mounted) {
43
+ return;
44
+ }
45
+ setError(nextError);
46
+ onError?.(nextError);
47
+ });
48
+ return () => {
49
+ mounted = false;
50
+ };
51
+ }, [appApi, onError]);
52
+ // --- Pure function deps ---
53
+ const reducer = useMemo(() => createReducer(appApi.treeNodeApi, appApi.foldNodeService), [appApi.treeNodeApi, appApi.foldNodeService]);
54
+ const intentToEffect = useMemo(() => createIntentToEffect(appApi.stateApi, appApi.cursorApi, appApi.treeNodeApi), [appApi.stateApi, appApi.cursorApi, appApi.treeNodeApi]);
55
+ const effectToAction = useMemo(() => createEffectToAction(appApi.treeNodeApi, appApi.cursorApi, appApi.navNodeApi), [appApi.treeNodeApi, appApi.navNodeApi, appApi.cursorApi]);
56
+ // --- JSX ---
57
+ if (error !== undefined) {
58
+ return _jsx(Text, { color: "red", children: error.message });
59
+ }
60
+ if (empty) {
61
+ return _jsx(Text, { dimColor: true, children: appApi.emptyForestMessage });
62
+ }
63
+ if (initialState === undefined) {
64
+ return _jsx(Text, { dimColor: true, children: "Loading." });
65
+ }
66
+ return (_jsx(App, { appApi: appApi, initialState: initialState, reducer: reducer, intentToEffect: intentToEffect, effectToAction: effectToAction, ...(print === undefined ? {} : { print }), ...(onError === undefined ? {} : { onError }) }));
67
+ }
@@ -0,0 +1,8 @@
1
+ import { InputState } from 'dirvi-lib';
2
+ export declare const STATUS_BAR_HEIGHT = 1;
3
+ type StatusBarProps = {
4
+ inputState: InputState;
5
+ };
6
+ export declare function StatusBar({ inputState }: StatusBarProps): import("react").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=StatusBar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StatusBar.d.ts","sourceRoot":"","sources":["../../../src/ui/components/StatusBar.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC,KAAK,cAAc,GAAG;IACpB,UAAU,EAAE,UAAU,CAAC;CACxB,CAAC;AAEF,wBAAgB,SAAS,CAAC,EAAE,UAAU,EAAE,EAAE,cAAc,+BAyBvD"}
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export const STATUS_BAR_HEIGHT = 1;
4
+ export function StatusBar({ inputState }) {
5
+ const inputMode = inputState.inputMode;
6
+ return (_jsxs(Box, { width: "100%", height: STATUS_BAR_HEIGHT, flexDirection: "row", justifyContent: "space-between", flexShrink: 0, backgroundColor: 'gray', children: [_jsx(Box, { flexShrink: 1, children: _jsx(Text, { color: "black", wrap: "truncate-end", children: inputMode === 'command' ? inputState.commandLine : '' }) }), _jsx(Box, { flexShrink: 1, children: _jsx(Text, { color: "black", wrap: "truncate-start", children: inputMode === 'normal' ? inputState.normalBuffer : '' }) })] }));
7
+ }
@@ -3,9 +3,9 @@ import { Box, Text } from 'ink';
3
3
  export function ViewRowComponent({ row }) {
4
4
  const prefix = '';
5
5
  switch (row.type) {
6
- case 'file':
6
+ case 'leaf':
7
7
  return (_jsx(Box, { paddingLeft: row.indent * 2, children: _jsxs(Text, { inverse: row.selected, children: [prefix, row.content] }) }));
8
- case 'directory':
8
+ case 'branch':
9
9
  return (_jsx(Box, { paddingLeft: row.indent * 2, children: _jsxs(Text, { inverse: row.selected, color: "blue", children: [prefix, row.content] }) }));
10
10
  case 'fold':
11
11
  return (_jsx(Box, { paddingLeft: row.indent * 2, children: _jsxs(Text, { inverse: row.selected, dimColor: true, children: [prefix, row.content] }) }));
@@ -1,4 +1,5 @@
1
- import { NavigationNode, State } from 'dirvi-lib';
1
+ import { CursorApi, NavNode, SerializableKey, State, TreeNode } from 'dirvi-lib';
2
2
  import { View } from '../view.js';
3
- export declare function useView(navigation: NavigationNode, state: State): View;
3
+ export type UseView = <Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>(navigation: NavNode<Id, BufferNode>, state: State<Id, BufferNode>, terminalRows: number) => View;
4
+ export declare function useView<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>(navigation: NavNode<Id, BufferNode>, state: State<Id, BufferNode>, cursorApi: CursorApi<Id>, terminalRows: number): View;
4
5
  //# sourceMappingURL=useView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useView.d.ts","sourceRoot":"","sources":["../../../src/ui/hooks/useView.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,cAAc,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,IAAI,EAAW,MAAM,YAAY,CAAC;AAO3C,wBAAgB,OAAO,CAAC,UAAU,EAAE,cAAc,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAmBtE"}
1
+ {"version":3,"file":"useView.d.ts","sourceRoot":"","sources":["../../../src/ui/hooks/useView.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,OAAO,EACP,eAAe,EACf,KAAK,EACL,QAAQ,EACT,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,IAAI,EAAW,MAAM,YAAY,CAAC;AAI3C,MAAM,MAAM,OAAO,GAAG,CACpB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAE3C,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE,UAAU,CAAC,EACnC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,EAC5B,YAAY,EAAE,MAAM,KACjB,IAAI,CAAC;AAIV,wBAAgB,OAAO,CACrB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAE3C,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE,UAAU,CAAC,EACnC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,EAC5B,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC,EACxB,YAAY,EAAE,MAAM,GACnB,IAAI,CAoBN"}
@@ -1,15 +1,14 @@
1
1
  import { View } from '../view.js';
2
- import { useWindowSize } from 'ink';
3
2
  import { useRef } from 'react';
3
+ import { STATUS_BAR_HEIGHT } from '../components/StatusBar.js';
4
4
  // Hooks that keeps Ref of the viewport's start, and returns View sliced to
5
5
  // only the rows that should be visible.
6
- export function useView(navigation, state) {
7
- const { rows: terminalRows } = useWindowSize();
6
+ export function useView(navigation, state, cursorApi, terminalRows) {
8
7
  const viewportStartRef = useRef(0);
9
- const viewportHeight = Math.max(1, terminalRows);
10
- const rows = View.createRows(navigation, state.cursor);
8
+ const viewportHeight = Math.max(1, terminalRows - STATUS_BAR_HEIGHT);
9
+ const rows = View.createRows(navigation, state.cursor, cursorApi);
11
10
  viewportStartRef.current = viewportStart(rows, viewportHeight, viewportStartRef.current);
12
- return View.create(navigation, state.cursor, viewportHeight, viewportStartRef.current);
11
+ return View.create(navigation, state.cursor, cursorApi, viewportHeight, viewportStartRef.current);
13
12
  }
14
13
  function viewportStart(rows, viewportHeight, currentViewportStart) {
15
14
  const scrollMargin = 3;
package/dist/ui/view.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { Cursor, NavigationNode } from 'dirvi-lib';
2
- export type ViewRowType = 'file' | 'directory' | 'fold';
1
+ import { CursorApi, SerializableKey, TreeNode } from 'dirvi-lib';
2
+ import { Cursor, NavNode } from 'dirvi-lib';
3
+ export type ViewRowType = 'leaf' | 'branch' | 'fold';
3
4
  export type ViewRow = {
4
5
  id: string;
5
6
  indent: number;
@@ -12,7 +13,7 @@ export type View = {
12
13
  rows: ViewRow[];
13
14
  };
14
15
  export declare const View: {
15
- createRows(navigation: NavigationNode, cursor: Cursor): ViewRow[];
16
- create(navigation: NavigationNode, cursor: Cursor, viewportHeight: number, viewportStart: number): View;
16
+ createRows<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>(navigation: NavNode<Id, BufferNode>, cursor: Cursor<Id>, cursorApi: CursorApi<Id>): ViewRow[];
17
+ create<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>(navigation: NavNode<Id, BufferNode>, cursor: Cursor<Id>, cursorApi: CursorApi<Id>, viewportHeight: number, viewportStart: number): View;
17
18
  };
18
19
  //# sourceMappingURL=view.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../src/ui/view.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EAEN,cAAc,EAEf,MAAM,WAAW,CAAC;AAEnB,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;AAMxD,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IACjB,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,IAAI;2BAED,cAAc,UAClB,MAAM,GACb,OAAO,EAAE;uBAKE,cAAc,UAClB,MAAM,kBACE,MAAM,iBACP,MAAM,GACpB,IAAI;CAUR,CAAC"}
1
+ {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../src/ui/view.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,EAAE,MAAM,EAAyB,OAAO,EAAE,MAAM,WAAW,CAAC;AAEnE,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAMrD,MAAM,MAAM,OAAO,GAAG;IAEpB,EAAE,EAAE,MAAM,CAAC;IAGX,MAAM,EAAE,MAAM,CAAC;IAGf,QAAQ,EAAE,OAAO,CAAC;IAGlB,MAAM,EAAE,OAAO,CAAC;IAGhB,OAAO,EAAE,MAAM,CAAC;IAGhB,IAAI,EAAE,WAAW,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IACjB,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,IAAI;eAEb,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,cAE/B,OAAO,CAAC,EAAE,EAAE,UAAU,CAAC,UAC3B,MAAM,CAAC,EAAE,CAAC,aACP,SAAS,CAAC,EAAE,CAAC,GACvB,OAAO,EAAE;WAKV,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,cAE/B,OAAO,CAAC,EAAE,EAAE,UAAU,CAAC,UAC3B,MAAM,CAAC,EAAE,CAAC,aACP,SAAS,CAAC,EAAE,CAAC,kBACR,MAAM,iBACP,MAAM,GACpB,IAAI;CAOR,CAAC"}
package/dist/ui/view.js CHANGED
@@ -1,36 +1,74 @@
1
- import { UnixEntryPath, } from 'dirvi-lib';
1
+ import { isNavBranch } from 'dirvi-lib';
2
2
  export const View = {
3
- createRows(navigation, cursor) {
4
- return viewRowsAtNode(navigation, [], cursor);
3
+ createRows(navigation, cursor, cursorApi) {
4
+ return viewRowsAtNode(navigation, [], cursor, cursorApi);
5
5
  },
6
- create(navigation, cursor, viewportHeight, viewportStart) {
7
- const rows = View.createRows(navigation, cursor);
6
+ create(navigation, cursor, cursorApi, viewportHeight, viewportStart) {
7
+ const rows = View.createRows(navigation, cursor, cursorApi);
8
8
  return {
9
9
  rows: rows.slice(viewportStart, viewportStart + viewportHeight),
10
10
  };
11
11
  },
12
12
  };
13
- function viewRowsAtNode(node, parentPath, cursor) {
13
+ /**
14
+ * Converts one NavNode and its descendants into display ViewRows.
15
+ *
16
+ * `node` is the directory currently being visited.
17
+ *
18
+ * `parentPath` is the path of that directory. For example:
19
+ *
20
+ * [] root directory
21
+ * ['src'] root/src
22
+ * ['src', 'lib'] root/src/lib
23
+ *
24
+ * The function uses a depth-first, pre-order traversal:
25
+ *
26
+ * 1. Add an entry's row.
27
+ * 2. If the entry is an opened branch, recursively add its children.
28
+ * 3. After all visible entries, add the directory's fold row.
29
+ *
30
+ * This ordering is what makes the flat row list visually represent a tree.
31
+ */
32
+ function viewRowsAtNode(node, parentPath, cursor, cursorApi) {
14
33
  const rows = [];
34
+ /*
35
+ * `node.entries` contains only the entries currently visible in this
36
+ * directory. Folded entries are kept separately in `node.foldedEntries`
37
+ * and are represented by one fold row added below.
38
+ */
15
39
  for (const entry of node.entries) {
16
- const isCursor = cursor.kind === 'entry' &&
17
- UnixEntryPath.equal(cursor.parentPath, parentPath) &&
18
- cursor.entryName === entry.name;
40
+ const entryCursor = {
41
+ kind: 'entry',
42
+ parentPath,
43
+ entryId: entry.id,
44
+ };
45
+ // If cursor at entry, needs to be rendered differently
46
+ const isCursor = cursorApi.equal(cursor, entryCursor);
47
+ // Add entry before visiting branches
19
48
  rows.push(viewRowForEntry(entry, parentPath, isCursor));
20
- if (entry.kind !== 'directory' || entry.node === undefined) {
49
+ // Case where there's no branches
50
+ if (!isNavBranch(entry) || entry.children === null) {
21
51
  continue;
22
52
  }
23
- rows.push(...viewRowsAtNode(entry.node, [...parentPath, entry.name], cursor));
53
+ rows.push(...viewRowsAtNode(entry.children, [...parentPath, entry.id], cursor, cursorApi));
24
54
  }
55
+ /*
56
+ * Folded entries are not rendered individually. They are represented
57
+ * by one synthetic row placed after all visible entries and descendants
58
+ * of this directory.
59
+ */
25
60
  if (node.foldedEntries.length > 0) {
26
- const isCursor = cursor.kind === 'fold' &&
27
- UnixEntryPath.equal(cursor.parentPath, parentPath);
61
+ const foldCursor = {
62
+ kind: 'fold',
63
+ parentPath,
64
+ };
65
+ const isCursor = cursorApi.equal(cursor, foldCursor);
28
66
  rows.push({
29
67
  id: foldId(parentPath),
30
68
  indent: parentPath.length,
31
69
  selected: isCursor,
32
70
  cursor: isCursor,
33
- content: `⋯ ${node.foldedEntries.length} folded entries`,
71
+ content: `⋯ ${node.foldedEntries.length} folded`,
34
72
  type: 'fold',
35
73
  });
36
74
  }
@@ -38,7 +76,7 @@ function viewRowsAtNode(node, parentPath, cursor) {
38
76
  }
39
77
  function viewRowForEntry(entry, parentPath, cursor) {
40
78
  return {
41
- id: entryId(parentPath, entry.name),
79
+ id: entryId(parentPath, entry.id),
42
80
  indent: parentPath.length,
43
81
  selected: cursor,
44
82
  cursor,
@@ -53,22 +91,8 @@ function foldId(parentPath) {
53
91
  return `fold:${JSON.stringify(parentPath)}`;
54
92
  }
55
93
  function content(entry) {
56
- switch (entry.kind) {
57
- case 'file':
58
- return entry.name;
59
- case 'directory':
60
- return `${entry.name}/`;
61
- case 'symlink':
62
- return `${entry.name} -> ${entry.target}`;
63
- }
94
+ return isNavBranch(entry) ? `${String(entry.id)}/` : String(entry.id);
64
95
  }
65
96
  function type(entry) {
66
- switch (entry.kind) {
67
- case 'file':
68
- return 'file';
69
- case 'directory':
70
- return 'directory';
71
- case 'symlink':
72
- return 'file';
73
- }
97
+ return isNavBranch(entry) ? 'branch' : 'leaf';
74
98
  }