@mak-98/dirvi-cli 0.6.0 → 0.7.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 (42) hide show
  1. package/dist/domain/app-api.d.ts +7 -3
  2. package/dist/domain/app-api.d.ts.map +1 -1
  3. package/dist/domain/posix-tree-node.d.ts +33 -0
  4. package/dist/domain/posix-tree-node.d.ts.map +1 -0
  5. package/dist/domain/posix-tree-node.js +53 -0
  6. package/dist/domain/unix-path.d.ts +6 -0
  7. package/dist/domain/unix-path.d.ts.map +1 -0
  8. package/dist/domain/unix-path.js +11 -0
  9. package/dist/domain/view-api.d.ts +11 -0
  10. package/dist/domain/view-api.d.ts.map +1 -0
  11. package/dist/index.js +2 -4
  12. package/dist/infrastructure/create-file-view-api.d.ts +30 -0
  13. package/dist/infrastructure/create-file-view-api.d.ts.map +1 -0
  14. package/dist/infrastructure/create-file-view-api.js +154 -0
  15. package/dist/infrastructure/get-dir-entries.d.ts +3 -2
  16. package/dist/infrastructure/get-dir-entries.d.ts.map +1 -1
  17. package/dist/infrastructure/get-dir-entries.js +2 -1
  18. package/dist/infrastructure/get-unix-abs-path.d.ts +1 -1
  19. package/dist/infrastructure/get-unix-abs-path.d.ts.map +1 -1
  20. package/dist/infrastructure/get-unix-abs-path.js +1 -1
  21. package/dist/infrastructure/load-posix-app-api.d.ts +15 -2
  22. package/dist/infrastructure/load-posix-app-api.d.ts.map +1 -1
  23. package/dist/infrastructure/load-posix-app-api.js +82 -2
  24. package/dist/infrastructure/view-name-schema.d.ts +18 -0
  25. package/dist/infrastructure/view-name-schema.d.ts.map +1 -0
  26. package/dist/infrastructure/view-name-schema.js +68 -0
  27. package/dist/ui/App.d.ts +3 -3
  28. package/dist/ui/App.d.ts.map +1 -1
  29. package/dist/ui/App.js +46 -0
  30. package/dist/ui/AppShell.d.ts +1 -1
  31. package/dist/ui/AppShell.d.ts.map +1 -1
  32. package/dist/ui/AppShell.js +1 -1
  33. package/dist/ui/LoadingApp.d.ts +3 -3
  34. package/dist/ui/LoadingApp.d.ts.map +1 -1
  35. package/dist/ui/LoadingApp.js +1 -1
  36. package/package.json +5 -3
  37. package/dist/domain/event-message.d.ts +0 -12
  38. package/dist/domain/event-message.d.ts.map +0 -1
  39. package/dist/infrastructure/get-dir-lazy-entries.d.ts +0 -3
  40. package/dist/infrastructure/get-dir-lazy-entries.d.ts.map +0 -1
  41. package/dist/infrastructure/get-dir-lazy-entries.js +0 -33
  42. /package/dist/domain/{event-message.js → view-api.js} +0 -0
@@ -1,7 +1,9 @@
1
- import { CursorApi, FoldNodeApi, FoldNodeService, NavNodeApi, SerializableKey, StateApi, TreeNode, TreeNodeApi } from 'dirvi-lib';
2
- export type AppApi<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = {
1
+ import { CursorApi, FoldNodeApi, FoldNodeService, NavNodeApi, SerializableKey, State, StateApi, TreeNode, TreeNodeApi } from 'dirvi-lib';
2
+ import { ViewApi } from './view-api.js';
3
+ export type AppApi<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>, ViewKey = string> = {
4
+ appId: string;
3
5
  /**
4
- * Name displayed to distinguish apps.
6
+ * Name displayed to distinguish app instance.
5
7
  */
6
8
  name: string;
7
9
  /**
@@ -35,6 +37,8 @@ export type AppApi<Id extends SerializableKey, BufferNode extends TreeNode<Id, B
35
37
  cursorApi: CursorApi<Id>;
36
38
  stateApi: StateApi<Id, BufferNode>;
37
39
  navNodeApi: NavNodeApi<Id, BufferNode>;
40
+ viewKey: ViewKey;
41
+ viewApi: ViewApi<State<Id, BufferNode>, ViewKey>;
38
42
  };
39
43
  export declare function createAppApis<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>(): {
40
44
  treeNodeApi: TreeNodeApi<Id, BufferNode>;
@@ -1 +1 @@
1
- {"version":3,"file":"app-api.d.ts","sourceRoot":"","sources":["../../src/domain/app-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,SAAS,EACT,WAAW,EACX,eAAe,EACf,UAAU,EACV,eAAe,EACf,QAAQ,EACR,QAAQ,EACR,WAAW,EACZ,MAAM,WAAW,CAAC;AAEnB,MAAM,MAAM,MAAM,CAChB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,IACzC;IACF;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAE3B;;;;OAIG;IACH,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAEpD;;;;;OAKG;IACH,iBAAiB,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IAExD,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAEzC;;OAEG;IACH,WAAW,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;IAE7B;;;OAGG;IACH,eAAe,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC;IAErC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;IAEzB,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAEnC,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;CACxC,CAAC;AAEF,wBAAgB,aAAa,CAC3B,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,KACxC;IACH,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IACzC,WAAW,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;IAC7B,eAAe,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC;IACrC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;IACzB,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IACnC,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;CACxC,CAkCA"}
1
+ {"version":3,"file":"app-api.d.ts","sourceRoot":"","sources":["../../src/domain/app-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,SAAS,EACT,WAAW,EACX,eAAe,EACf,UAAU,EACV,eAAe,EACf,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,MAAM,MAAM,MAAM,CAChB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,OAAO,GAAG,MAAM,IACd;IAIF,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAE3B;;;;OAIG;IACH,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAEpD;;;;;OAKG;IACH,iBAAiB,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IAExD,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAEzC;;OAEG;IACH,WAAW,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;IAE7B;;;OAGG;IACH,eAAe,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC;IAErC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;IAEzB,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAEnC,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAEvC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC;CAClD,CAAC;AAEF,wBAAgB,aAAa,CAC3B,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,KACxC;IACH,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IACzC,WAAW,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;IAC7B,eAAe,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC;IACrC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;IACzB,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IACnC,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;CACxC,CAkCA"}
@@ -0,0 +1,33 @@
1
+ import { z } from 'zod';
2
+ import { UnixPath } from './unix-path.js';
3
+ import { Cursor, FoldNode, FoldNodeRoot, NavNode, State } from 'dirvi-lib';
4
+ export declare const PosixNameSchema: z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodString>;
5
+ export type PosixName = z.output<typeof PosixNameSchema>;
6
+ export declare const PosixTreeNodeSchema: z.ZodType<PosixTreeNode>;
7
+ export type PosixTreeNode = {
8
+ kind: 'file';
9
+ id: PosixName;
10
+ } | {
11
+ kind: 'symlink';
12
+ id: PosixName;
13
+ target: UnixPath;
14
+ } | {
15
+ kind: 'directory';
16
+ id: PosixName;
17
+ children: PosixTreeNode[] | null;
18
+ };
19
+ export declare const PosixTreeNodeApi: import("dirvi-lib").TreeNodeApi<string, PosixTreeNode>;
20
+ export declare const PosixFoldNodeSchema: z.ZodType<PosixFoldNode>;
21
+ export declare const PosixFoldNodeRootSchema: z.ZodType<PosixFoldNodeRoot>;
22
+ export type PosixFoldNode = FoldNode<PosixName>;
23
+ export type PosixFoldNodeRoot = FoldNodeRoot<PosixName>;
24
+ export declare const PosixFoldNodeApi: import("dirvi-lib").FoldNodeApi<string>;
25
+ export declare const PosixCursorSchema: z.ZodType<PosixCursor>;
26
+ export type PosixCursor = Cursor<PosixName>;
27
+ export declare const PosixCursorApi: import("dirvi-lib").CursorApi<string>;
28
+ export type PosixNavNode = NavNode<PosixName, PosixTreeNode>;
29
+ export declare const PosixNavApi: import("dirvi-lib").NavNodeApi<string, PosixTreeNode>;
30
+ export declare const PosixStateSchema: z.ZodType<PosixState>;
31
+ export type PosixState = State<PosixName, PosixTreeNode>;
32
+ export declare const PosixStateApi: import("dirvi-lib").StateApi<string, PosixTreeNode>;
33
+ //# sourceMappingURL=posix-tree-node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posix-tree-node.d.ts","sourceRoot":"","sources":["../../src/domain/posix-tree-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAkB,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAUL,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,OAAO,EAEP,KAAK,EAEN,MAAM,WAAW,CAAC;AAQnB,eAAO,MAAM,eAAe,yEAU3B,CAAC;AAGF,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAUzD,eAAO,MAAM,mBAAmB,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAsBvD,CAAC;AAGH,MAAM,MAAM,aAAa,GACrB;IACE,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,SAAS,CAAC;CACf,GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,EAAE,QAAQ,CAAC;CAClB,GACD;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,EAAE,EAAE,SAAS,CAAC;IACd,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;CAClC,CAAC;AAQN,eAAO,MAAM,gBAAgB,wDAAgD,CAAC;AAQ9E,eAAO,MAAM,mBAAmB,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAAkB,CAAC;AAC5E,eAAO,MAAM,uBAAuB,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAC7C,CAAC;AAGrB,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;AAChD,MAAM,MAAM,iBAAiB,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;AAGxD,eAAO,MAAM,gBAAgB,yCAAiC,CAAC;AAK/D,eAAO,MAAM,iBAAiB,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAChB,CAAC;AAGtC,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;AAE5C,eAAO,MAAM,cAAc,uCAA+B,CAAC;AAG3D,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;AAE7D,eAAO,MAAM,WAAW,uDAIvB,CAAC;AAKF,eAAO,MAAM,gBAAgB,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAIlD,CAAC;AAGF,MAAM,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;AAGzD,eAAO,MAAM,aAAa,qDAKzB,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { z } from 'zod';
2
+ import { UnixPathSchema } from './unix-path.js';
3
+ import { createCursorApi, createCursorSchema, createFoldNodeApi, createFoldNodeSchemas, createNavNodeApi, createStateApi, createStateSchema, createTreeNodeApi, createTreeNodeSchemas, serializableKeySchema, } from 'dirvi-lib';
4
+ // --- PosixName extends PropertyKey ---
5
+ // Schema
6
+ export const PosixNameSchema = serializableKeySchema.pipe(z
7
+ .string()
8
+ .min(1, 'Entry name cannot be empty.')
9
+ .refine((value) => !value.includes('/'), {
10
+ message: "Entry name must not contain '/'.",
11
+ })
12
+ .refine((value) => value !== '.' && value !== '..', {
13
+ message: "Entry name cannot be '.' or '..'.",
14
+ }));
15
+ // --- PosixTreeNode extends TreeNode ---
16
+ // Schema
17
+ export const PosixTreeNodeSchema = z.lazy(() => {
18
+ const { leafSchema, closedBranchSchema, openBranchSchema } = createTreeNodeSchemas(PosixNameSchema, PosixTreeNodeSchema);
19
+ return z.union([
20
+ leafSchema.extend({
21
+ kind: z.literal('file'),
22
+ }),
23
+ leafSchema.extend({
24
+ kind: z.literal('symlink'),
25
+ target: UnixPathSchema,
26
+ }),
27
+ closedBranchSchema.extend({
28
+ kind: z.literal('directory'),
29
+ }),
30
+ openBranchSchema.extend({
31
+ kind: z.literal('directory'),
32
+ }),
33
+ ]);
34
+ });
35
+ // API
36
+ export const PosixTreeNodeApi = createTreeNodeApi();
37
+ // --- PosixFoldNode ---
38
+ // Schema
39
+ const { foldNodeSchema, foldNodeRootSchema } = createFoldNodeSchemas(PosixNameSchema);
40
+ export const PosixFoldNodeSchema = foldNodeSchema;
41
+ export const PosixFoldNodeRootSchema = foldNodeRootSchema;
42
+ // API
43
+ export const PosixFoldNodeApi = createFoldNodeApi();
44
+ // --- PosixCursor ---
45
+ // Schema
46
+ export const PosixCursorSchema = createCursorSchema(PosixNameSchema);
47
+ export const PosixCursorApi = createCursorApi();
48
+ export const PosixNavApi = createNavNodeApi(PosixTreeNodeApi, PosixFoldNodeApi, PosixCursorApi);
49
+ // --- PosixState ---
50
+ // Schema
51
+ export const PosixStateSchema = createStateSchema(PosixTreeNodeSchema, PosixFoldNodeRootSchema, PosixCursorSchema);
52
+ // API
53
+ export const PosixStateApi = createStateApi(PosixTreeNodeApi, PosixFoldNodeApi, PosixCursorApi, PosixNavApi);
@@ -0,0 +1,6 @@
1
+ import { z } from 'zod';
2
+ export declare const UnixPathSchema: z.ZodString;
3
+ export type UnixPath = z.output<typeof UnixPathSchema>;
4
+ export declare const UnixAbsolutePathSchema: z.ZodString;
5
+ export type UnixAbsolutePath = z.output<typeof UnixAbsolutePathSchema>;
6
+ //# sourceMappingURL=unix-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unix-path.d.ts","sourceRoot":"","sources":["../../src/domain/unix-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,cAAc,aAKvB,CAAC;AAEL,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAEvD,eAAO,MAAM,sBAAsB,aAKlC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+ // UNIX PATHS
3
+ export const UnixPathSchema = z
4
+ .string()
5
+ .min(1)
6
+ .refine((value) => !value.includes('\0'), {
7
+ message: 'Unix paths must not contain null bytes',
8
+ });
9
+ export const UnixAbsolutePathSchema = UnixPathSchema.refine((value) => value.startsWith('/'), {
10
+ message: 'Path must be an absolute Unix path',
11
+ });
@@ -0,0 +1,11 @@
1
+ export type ViewSummary = {
2
+ name: string;
3
+ updatedAt: string;
4
+ };
5
+ export type ViewApi<State, Key> = {
6
+ save: (key: Key, name: string, state: State) => Promise<void>;
7
+ load: (key: Key, name: string) => Promise<State | undefined>;
8
+ list: (key: Key) => Promise<readonly ViewSummary[]>;
9
+ remove: (key: Key, name: string) => Promise<void>;
10
+ };
11
+ //# sourceMappingURL=view-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-api.d.ts","sourceRoot":"","sources":["../../src/domain/view-api.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,OAAO,CAAC,KAAK,EAAE,GAAG,IAAI;IAChC,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IAC7D,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAC;IACpD,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD,CAAC"}
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ process.on('exit', restoreTerminal);
28
28
  program
29
29
  .name('direx')
30
30
  .description('View and manage directories.')
31
- .version('0.5.1')
31
+ .version('0.7.0')
32
32
  .helpOption('--help')
33
33
  .option('-d, --directory <path>', 'Directory to browse')
34
34
  .action(async () => {
@@ -59,9 +59,7 @@ program
59
59
  try {
60
60
  const app = render(_jsx(AppShell, { ...(options.directory === undefined
61
61
  ? {}
62
- : { directory: options.directory }), ...(stdoutIsInteractive
63
- ? {}
64
- : { stdout }), clipboard: clipboard, onError: (error) => {
62
+ : { directory: options.directory }), ...(stdoutIsInteractive ? {} : { stdout }), clipboard: clipboard, onError: (error) => {
65
63
  appError = error;
66
64
  } }), { stdout: uiOutput });
67
65
  // Errors thrown while leaving this await still execute the finally block.
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod';
2
+ import type { ViewApi } from '../domain/view-api.js';
3
+ import { SerializableKey, State, TreeNode } from 'dirvi-lib';
4
+ export type StateCodec<RuntimeState, StoredState> = {
5
+ schema: z.ZodType<StoredState>;
6
+ encode: (state: RuntimeState) => StoredState;
7
+ decode: (state: StoredState) => RuntimeState;
8
+ };
9
+ /**
10
+ * Creates a file-backed implementation of ViewApi.
11
+ *
12
+ * Each key receives its own directory, and each named view is stored as one
13
+ * JSON file inside that directory:
14
+ *
15
+ * <directory>/<encoded-key>/<view-name>.json
16
+ *
17
+ * The view name is validated before being used as a filename. The key is
18
+ * converted into one safe path segment by the caller-provided `encodeKey`
19
+ * function.
20
+ *
21
+ * For example:
22
+ *
23
+ * .../posix/views/<directory-hash>/project.json
24
+ */
25
+ export declare function createFileViewApi<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>, StoredState, Key>({ directory, encodeKey, stateCodec, }: {
26
+ directory: string;
27
+ encodeKey: (key: Key) => string;
28
+ stateCodec: StateCodec<State<Id, BufferNode>, StoredState>;
29
+ }): ViewApi<State<Id, BufferNode>, Key>;
30
+ //# sourceMappingURL=create-file-view-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-file-view-api.d.ts","sourceRoot":"","sources":["../../src/infrastructure/create-file-view-api.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,OAAO,EAAe,MAAM,uBAAuB,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE7D,MAAM,MAAM,UAAU,CAAC,YAAY,EAAE,WAAW,IAAI;IAClD,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/B,MAAM,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,WAAW,CAAC;IAC7C,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,YAAY,CAAC;CAC9C,CAAC;AA2BF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,WAAW,EACX,GAAG,EACH,EACA,SAAS,EACT,SAAS,EACT,UAAU,GACX,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,CAAC;IAChC,UAAU,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,EAAE,WAAW,CAAC,CAAC;CAC5D,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,EAAE,GAAG,CAAC,CA0JtC"}
@@ -0,0 +1,154 @@
1
+ import { mkdir, readFile, readdir, rename, unlink, writeFile, } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { viewNameSchema } from './view-name-schema.js';
6
+ function createStoredViewSchema(stateSchema) {
7
+ return z.object({
8
+ version: z.literal(1),
9
+ name: viewNameSchema,
10
+ updatedAt: z.iso.datetime(),
11
+ state: stateSchema,
12
+ });
13
+ }
14
+ function isNodeErrorWithCode(error, code) {
15
+ return error instanceof Error && 'code' in error && error.code === code;
16
+ }
17
+ /**
18
+ * Creates a file-backed implementation of ViewApi.
19
+ *
20
+ * Each key receives its own directory, and each named view is stored as one
21
+ * JSON file inside that directory:
22
+ *
23
+ * <directory>/<encoded-key>/<view-name>.json
24
+ *
25
+ * The view name is validated before being used as a filename. The key is
26
+ * converted into one safe path segment by the caller-provided `encodeKey`
27
+ * function.
28
+ *
29
+ * For example:
30
+ *
31
+ * .../posix/views/<directory-hash>/project.json
32
+ */
33
+ export function createFileViewApi({ directory, encodeKey, stateCodec, }) {
34
+ const storedViewSchema = createStoredViewSchema(stateCodec.schema);
35
+ /**
36
+ * Returns the directory containing all views for one app-specific key.
37
+ *
38
+ * `encodeKey` must return one safe path segment. In particular, it must not
39
+ * return "/", "\\", ".", "..", or a value containing path separators.
40
+ */
41
+ function keyDirectory(key) {
42
+ const encodedKey = encodeKey(key);
43
+ if (encodedKey.length === 0 ||
44
+ encodedKey === '.' ||
45
+ encodedKey === '..' ||
46
+ encodedKey.includes('/') ||
47
+ encodedKey.includes('\\')) {
48
+ throw new Error('encodeKey must return one safe path segment.');
49
+ }
50
+ return join(directory, encodedKey);
51
+ }
52
+ /**
53
+ * Returns the file containing one named view for a key.
54
+ *
55
+ * View names are validated by `viewNameSchema` before they are used as
56
+ * filenames. No encoding is necessary because invalid path characters have
57
+ * already been rejected.
58
+ */
59
+ function viewFile(key, name) {
60
+ const validName = viewNameSchema.parse(name);
61
+ return join(keyDirectory(key), `${validName}.json`);
62
+ }
63
+ async function save(key, name, state) {
64
+ const dir = keyDirectory(key);
65
+ await mkdir(dir, {
66
+ recursive: true,
67
+ mode: 0o700,
68
+ });
69
+ const file = viewFile(key, name);
70
+ const storedState = stateCodec.encode(state);
71
+ const value = storedViewSchema.parse({
72
+ version: 1,
73
+ name,
74
+ updatedAt: new Date().toISOString(),
75
+ state: storedState,
76
+ });
77
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
78
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, {
79
+ encoding: 'utf8',
80
+ mode: 0o600,
81
+ });
82
+ await rename(temporary, file);
83
+ }
84
+ async function load(key, name) {
85
+ try {
86
+ const contents = await readFile(viewFile(key, name), 'utf8');
87
+ const parsed = JSON.parse(contents);
88
+ const value = storedViewSchema.parse(parsed);
89
+ return stateCodec.decode(value.state);
90
+ }
91
+ catch (error) {
92
+ if (isNodeErrorWithCode(error, 'ENOENT')) {
93
+ return undefined;
94
+ }
95
+ throw error;
96
+ }
97
+ }
98
+ async function list(key) {
99
+ const dir = keyDirectory(key);
100
+ let files;
101
+ try {
102
+ files = await readdir(dir);
103
+ }
104
+ catch (error) {
105
+ // A key with no saved views does not have a directory yet.
106
+ if (isNodeErrorWithCode(error, 'ENOENT')) {
107
+ return [];
108
+ }
109
+ throw error;
110
+ }
111
+ const summaries = [];
112
+ for (const file of files) {
113
+ /*
114
+ * Ignore temporary files, unrelated files, and any future files that
115
+ * might be stored alongside view files.
116
+ */
117
+ if (!file.endsWith('.json')) {
118
+ continue;
119
+ }
120
+ /*
121
+ * The name and timestamp are stored in the JSON envelope. Listing
122
+ * therefore requires reading each view file. If this becomes expensive
123
+ * later, metadata could be kept in a separate index.
124
+ */
125
+ const contents = await readFile(join(dir, file), 'utf8');
126
+ const parsed = JSON.parse(contents);
127
+ const value = storedViewSchema.parse(parsed);
128
+ summaries.push({
129
+ name: value.name,
130
+ updatedAt: value.updatedAt,
131
+ });
132
+ }
133
+ return summaries;
134
+ }
135
+ async function remove(key, name) {
136
+ try {
137
+ // viewFile() validates the name before constructing the path.
138
+ await unlink(viewFile(key, name));
139
+ }
140
+ catch (error) {
141
+ // Removing a view that does not exist is intentionally idempotent.
142
+ if (isNodeErrorWithCode(error, 'ENOENT')) {
143
+ return;
144
+ }
145
+ throw error;
146
+ }
147
+ }
148
+ return {
149
+ save,
150
+ load,
151
+ list,
152
+ remove,
153
+ };
154
+ }
@@ -1,3 +1,4 @@
1
- import type { UnixAbsolutePath, PosixNode } from 'dirvi-lib';
2
- export declare function getDirEntries(address: UnixAbsolutePath): Promise<PosixNode[]>;
1
+ import { UnixAbsolutePath } from '../domain/unix-path.js';
2
+ import { PosixTreeNode } from '../domain/posix-tree-node.js';
3
+ export declare function getDirEntries(address: UnixAbsolutePath): Promise<PosixTreeNode[]>;
3
4
  //# sourceMappingURL=get-dir-entries.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"get-dir-entries.d.ts","sourceRoot":"","sources":["../../src/infrastructure/get-dir-entries.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG7D,wBAAsB,aAAa,CACjC,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,SAAS,EAAE,CAAC,CAyCtB"}
1
+ {"version":3,"file":"get-dir-entries.d.ts","sourceRoot":"","sources":["../../src/infrastructure/get-dir-entries.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAkB,MAAM,wBAAwB,CAAC;AAC1E,OAAO,EAAmB,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE9E,wBAAsB,aAAa,CACjC,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,aAAa,EAAE,CAAC,CAyC1B"}
@@ -1,6 +1,7 @@
1
1
  import { readdir, readlink } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { UnixPathSchema, PosixNameSchema } from 'dirvi-lib';
3
+ import { UnixPathSchema } from '../domain/unix-path.js';
4
+ import { PosixNameSchema } from '../domain/posix-tree-node.js';
4
5
  export async function getDirEntries(address) {
5
6
  const directoryEntries = await readdir(address, {
6
7
  withFileTypes: true,
@@ -1,3 +1,3 @@
1
- import type { UnixAbsolutePath } from 'dirvi-lib';
1
+ import { UnixAbsolutePath } from '../domain/unix-path.js';
2
2
  export declare function getUnixAbsPath(directory: string): UnixAbsolutePath;
3
3
  //# sourceMappingURL=get-unix-abs-path.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"get-unix-abs-path.d.ts","sourceRoot":"","sources":["../../src/infrastructure/get-unix-abs-path.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGlD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAgBlE"}
1
+ {"version":3,"file":"get-unix-abs-path.d.ts","sourceRoot":"","sources":["../../src/infrastructure/get-unix-abs-path.ts"],"names":[],"mappings":"AACA,OAAO,EACL,gBAAgB,EAEjB,MAAM,wBAAwB,CAAC;AAEhC,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAgBlE"}
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import { UnixAbsolutePathSchema } from 'dirvi-lib';
2
+ import { UnixAbsolutePathSchema, } from '../domain/unix-path.js';
3
3
  export function getUnixAbsPath(directory) {
4
4
  const absolutePath = resolve(directory);
5
5
  const result = UnixAbsolutePathSchema.safeParse(absolutePath);
@@ -1,4 +1,17 @@
1
- import { PosixName, PosixNode } from 'dirvi-lib';
2
1
  import { AppApi } from '../domain/app-api.js';
3
- export declare function loadPosixAppApi(directory?: string): AppApi<PosixName, PosixNode>;
2
+ import { PosixName, PosixState, PosixTreeNode } from '../domain/posix-tree-node.js';
3
+ import { UnixAbsolutePath } from '../domain/unix-path.js';
4
+ export type StoredPosixFoldNode = {
5
+ id: PosixName;
6
+ children: StoredPosixFoldNode[];
7
+ folds: PosixName[];
8
+ };
9
+ export type StoredPosixFoldNodeRoot = {
10
+ children: StoredPosixFoldNode[];
11
+ folds: PosixName[];
12
+ };
13
+ export type StoredPosixState = Omit<PosixState, 'foldNode'> & {
14
+ foldNode: StoredPosixFoldNodeRoot;
15
+ };
16
+ export declare function loadPosixAppApi(directory?: string): AppApi<PosixName, PosixTreeNode, UnixAbsolutePath>;
4
17
  //# sourceMappingURL=load-posix-app-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"load-posix-app-api.d.ts","sourceRoot":"","sources":["../../src/infrastructure/load-posix-app-api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,MAAM,EAAiB,MAAM,sBAAsB,CAAC;AAI7D,wBAAgB,eAAe,CAC7B,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,CAkB9B"}
1
+ {"version":3,"file":"load-posix-app-api.d.ts","sourceRoot":"","sources":["../../src/infrastructure/load-posix-app-api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAiB,MAAM,sBAAsB,CAAC;AAM7D,OAAO,EAIL,SAAS,EAET,UAAU,EACV,aAAa,EAEd,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAkB1D,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,SAAS,CAAC;IACd,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB,CAAC;AAsBF,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,GAAG;IAC5D,QAAQ,EAAE,uBAAuB,CAAC;CACnC,CAAC;AA2DF,wBAAgB,eAAe,CAC7B,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAAC,SAAS,EAAE,aAAa,EAAE,gBAAgB,CAAC,CAyBpD"}
@@ -3,18 +3,98 @@ import { watch } from 'node:fs';
3
3
  import { createAppApis } from '../domain/app-api.js';
4
4
  import { getUnixAbsPath } from './get-unix-abs-path.js';
5
5
  import { getDirEntries } from './get-dir-entries.js';
6
+ import envPaths from 'env-paths';
7
+ import { createFileViewApi } from './create-file-view-api.js';
8
+ import { createHash } from 'node:crypto';
9
+ import { PosixCursorSchema, PosixNameSchema, PosixTreeNodeSchema, } from '../domain/posix-tree-node.js';
10
+ import { z } from 'zod';
11
+ // ---*--- App Data ---*---
12
+ // Paths for app data:
13
+ const paths = envPaths('dirvi');
14
+ const posixAppDataDirectory = join(paths.data, 'apps', 'posix');
15
+ const posixAppViewsDirectory = join(posixAppDataDirectory, 'views');
16
+ // Helpers for saving app data:
17
+ // The key for the Posix app instance is just the directory working in.
18
+ function encodeKey(key) {
19
+ return createHash('sha256').update(key).digest('hex');
20
+ }
21
+ const StoredPosixFoldNodeSchema = z.lazy(() => z.object({
22
+ id: PosixNameSchema,
23
+ children: z.array(StoredPosixFoldNodeSchema),
24
+ folds: z.array(PosixNameSchema),
25
+ }));
26
+ const StoredPosixFoldNodeRootSchema = z.object({
27
+ children: z.array(StoredPosixFoldNodeSchema),
28
+ folds: z.array(PosixNameSchema),
29
+ });
30
+ const StoredPosixStateSchema = z.object({
31
+ buffer: z.array(PosixTreeNodeSchema),
32
+ foldNode: StoredPosixFoldNodeRootSchema,
33
+ cursor: PosixCursorSchema,
34
+ });
35
+ // ---*--- Codec ---*---
36
+ function encodePosixFoldNode(node) {
37
+ return {
38
+ id: node.id,
39
+ children: node.children.map(encodePosixFoldNode),
40
+ folds: [...node.folds],
41
+ };
42
+ }
43
+ function decodePosixFoldNode(node) {
44
+ return {
45
+ id: node.id,
46
+ children: node.children.map(decodePosixFoldNode),
47
+ folds: new Set(node.folds),
48
+ };
49
+ }
50
+ function encodePosixFoldNodeRoot(root) {
51
+ return {
52
+ children: root.children.map(encodePosixFoldNode),
53
+ folds: [...root.folds],
54
+ };
55
+ }
56
+ function decodePosixFoldNodeRoot(root) {
57
+ return {
58
+ children: root.children.map(decodePosixFoldNode),
59
+ folds: new Set(root.folds),
60
+ };
61
+ }
62
+ const posixStateCodec = {
63
+ schema: StoredPosixStateSchema,
64
+ encode(state) {
65
+ return {
66
+ buffer: state.buffer,
67
+ foldNode: encodePosixFoldNodeRoot(state.foldNode),
68
+ cursor: state.cursor,
69
+ };
70
+ },
71
+ decode(state) {
72
+ return {
73
+ buffer: state.buffer,
74
+ foldNode: decodePosixFoldNodeRoot(state.foldNode),
75
+ cursor: state.cursor,
76
+ };
77
+ },
78
+ };
79
+ // --- Creating Posix App API ---
6
80
  export function loadPosixAppApi(directory) {
7
81
  const unixAbsPath = getUnixAbsPath(directory ?? process.cwd());
8
82
  const apis = createAppApis();
9
- const subscribeToResync = createFsResyncSubscription(join(unixAbsPath));
10
83
  return {
84
+ appId: 'posix',
11
85
  name: `Posix(${unixAbsPath})`,
12
86
  emptyForestMessage: 'The directory is empty.',
13
87
  loadBranches: (path) => {
14
88
  const address = join(unixAbsPath, ...path);
15
89
  return getDirEntries(address);
16
90
  },
17
- subscribeToResync,
91
+ subscribeToResync: createFsResyncSubscription(join(unixAbsPath)),
92
+ viewKey: unixAbsPath,
93
+ viewApi: createFileViewApi({
94
+ directory: posixAppViewsDirectory,
95
+ encodeKey,
96
+ stateCodec: posixStateCodec,
97
+ }),
18
98
  ...apis,
19
99
  };
20
100
  }
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * View names are also used as filenames.
4
+ *
5
+ * We intentionally keep the accepted format human-readable instead of
6
+ * encoding names into opaque filenames. The restrictions are mostly for
7
+ * portability between POSIX and Windows filesystems:
8
+ *
9
+ * - no path separators or control characters;
10
+ * - no Windows-invalid filename characters;
11
+ * - no leading/trailing whitespace;
12
+ * - no trailing periods or spaces;
13
+ * - no "." or "..";
14
+ * - no Windows device names such as "CON" or "NUL";
15
+ * - limited length.
16
+ */
17
+ export declare const viewNameSchema: z.ZodString;
18
+ //# sourceMappingURL=view-name-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-name-schema.d.ts","sourceRoot":"","sources":["../../src/infrastructure/view-name-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2BxB;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,aA8BxB,CAAC"}
@@ -0,0 +1,68 @@
1
+ import { z } from 'zod';
2
+ const windowsReservedNames = new Set([
3
+ 'CON',
4
+ 'PRN',
5
+ 'AUX',
6
+ 'NUL',
7
+ 'COM1',
8
+ 'COM2',
9
+ 'COM3',
10
+ 'COM4',
11
+ 'COM5',
12
+ 'COM6',
13
+ 'COM7',
14
+ 'COM8',
15
+ 'COM9',
16
+ 'LPT1',
17
+ 'LPT2',
18
+ 'LPT3',
19
+ 'LPT4',
20
+ 'LPT5',
21
+ 'LPT6',
22
+ 'LPT7',
23
+ 'LPT8',
24
+ 'LPT9',
25
+ ]);
26
+ /**
27
+ * View names are also used as filenames.
28
+ *
29
+ * We intentionally keep the accepted format human-readable instead of
30
+ * encoding names into opaque filenames. The restrictions are mostly for
31
+ * portability between POSIX and Windows filesystems:
32
+ *
33
+ * - no path separators or control characters;
34
+ * - no Windows-invalid filename characters;
35
+ * - no leading/trailing whitespace;
36
+ * - no trailing periods or spaces;
37
+ * - no "." or "..";
38
+ * - no Windows device names such as "CON" or "NUL";
39
+ * - limited length.
40
+ */
41
+ export const viewNameSchema = z
42
+ .string()
43
+ .min(1, 'View name cannot be empty.')
44
+ .max(100, 'View name cannot be longer than 100 characters.')
45
+ .refine((name) => name === name.trim(), {
46
+ message: 'View name cannot start or end with whitespace.',
47
+ })
48
+ .refine((name) => name !== '.' && name !== '..', {
49
+ message: 'View name cannot be "." or "..".',
50
+ })
51
+ .refine((name) => !/[\/\\]/u.test(name), {
52
+ message: 'View name cannot contain path separators.',
53
+ })
54
+ .refine((name) => !/[\u0000-\u001f\u007f]/u.test(name), {
55
+ message: 'View name cannot contain control characters.',
56
+ })
57
+ .refine((name) => !/[<>:"|?*]/u.test(name), {
58
+ message: 'View name contains a character that is not portable.',
59
+ })
60
+ .refine((name) => !/[. ]$/u.test(name), {
61
+ message: 'View name cannot end with a period or space.',
62
+ })
63
+ .refine((name) => {
64
+ const basename = name.split('.')[0].toUpperCase();
65
+ return !windowsReservedNames.has(basename);
66
+ }, {
67
+ message: 'View name is reserved by Windows.',
68
+ });
package/dist/ui/App.d.ts CHANGED
@@ -2,8 +2,8 @@ import { IntentToEffect, SerializableKey, State, TreeNode } from 'dirvi-lib';
2
2
  import { EffectToAction } from '../application/effect-to-action.js';
3
3
  import { AppApi } from '../domain/app-api.js';
4
4
  import { Reducer } from '../application/reducer.js';
5
- type AppProps<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = {
6
- appApi: AppApi<Id, BufferNode>;
5
+ type AppProps<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>, ViewKey = string> = {
6
+ appApi: AppApi<Id, BufferNode, ViewKey>;
7
7
  initialState: State<Id, BufferNode>;
8
8
  reducer: Reducer<Id, BufferNode>;
9
9
  intentToEffect: IntentToEffect<Id, BufferNode>;
@@ -12,6 +12,6 @@ type AppProps<Id extends SerializableKey, BufferNode extends TreeNode<Id, Buffer
12
12
  clipboard?: (value: string) => void;
13
13
  onError?: (error: Error) => void;
14
14
  };
15
- export declare function App<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>({ appApi, initialState, reducer, intentToEffect, effectToAction, stdout, clipboard, onError, }: AppProps<Id, BufferNode>): import("react").JSX.Element | null;
15
+ export declare function App<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>, ViewKey = string>({ appApi, initialState, reducer, intentToEffect, effectToAction, stdout, clipboard, onError, }: AppProps<Id, BufferNode, ViewKey>): import("react").JSX.Element | null;
16
16
  export {};
17
17
  //# sourceMappingURL=App.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/ui/App.tsx"],"names":[],"mappings":"AAGA,OAAO,EAGL,cAAc,EACd,eAAe,EACf,KAAK,EACL,QAAQ,EAET,MAAM,WAAW,CAAC;AAGnB,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,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,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,MAAM,EACe,SAAS,EAC9B,OAAO,GACR,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,sCA0K1B"}
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/ui/App.tsx"],"names":[],"mappings":"AAGA,OAAO,EAGL,cAAc,EACd,eAAe,EACf,KAAK,EACL,QAAQ,EAET,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAGpE,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAOpD,KAAK,QAAQ,CACX,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,OAAO,GAAG,MAAM,IACd;IACF,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;IACxC,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,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,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,OAAO,GAAG,MAAM,EAChB,EACA,MAAM,EACN,YAAY,EACZ,OAAO,EACP,cAAc,EACd,cAAc,EACd,MAAM,EACN,SAAS,EACT,OAAO,GACR,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,sCA6NnC"}
package/dist/ui/App.js CHANGED
@@ -6,6 +6,9 @@ import { ViewRowComponent } from './components/ViewRowComponent.js';
6
6
  import { StatusBar } from './components/StatusBar.js';
7
7
  import { inkInputToUserInput } from '../infrastructure/ink-input-to-user-input.js';
8
8
  import { useView } from './hooks/useView.js';
9
+ function toError(error) {
10
+ return error instanceof Error ? error : new Error(String(error));
11
+ }
9
12
  export function App({ appApi, initialState, reducer, intentToEffect, effectToAction, stdout, clipboard, onError, }) {
10
13
  const [state, dispatch] = useReducer(reducer, initialState);
11
14
  // Give `subscribeToResync` callback a way to see current state:
@@ -94,6 +97,49 @@ export function App({ appApi, initialState, reducer, intentToEffect, effectToAct
94
97
  normalBuffer: '',
95
98
  });
96
99
  return;
100
+ case 'saveView': {
101
+ void appApi.viewApi
102
+ .save(appApi.viewKey, effect.name, stateRef.current)
103
+ .then(() => {
104
+ setInputState({
105
+ inputMode: 'normal',
106
+ normalBuffer: '',
107
+ });
108
+ })
109
+ .catch((error) => {
110
+ onError?.(toError(error));
111
+ setExitStatus(`Unable to save view: ${toError(error).message}`);
112
+ });
113
+ return;
114
+ }
115
+ case 'loadView': {
116
+ void appApi.viewApi
117
+ .load(appApi.viewKey, effect.name)
118
+ .then((loadedState) => {
119
+ if (loadedState === undefined) {
120
+ throw new Error(`View not found: ${effect.name}`);
121
+ }
122
+ // The saved buffer may be stale because the filesystem could have
123
+ // changed since the view was saved.
124
+ return appApi.stateApi.resync(loadedState, appApi.loadBranches);
125
+ })
126
+ .then((newState) => {
127
+ const oldState = stateRef.current;
128
+ dispatch({
129
+ kind: 'setState',
130
+ oldState,
131
+ newState,
132
+ });
133
+ setInputState({
134
+ inputMode: 'normal',
135
+ normalBuffer: '',
136
+ });
137
+ })
138
+ .catch((error) => {
139
+ onError?.(toError(error));
140
+ });
141
+ return;
142
+ }
97
143
  case 'quit':
98
144
  setExitStatus(effect.exitMessage);
99
145
  return;
@@ -4,5 +4,5 @@ export type ShellAppProps = {
4
4
  clipboard?: (value: string) => void;
5
5
  onError?: (error: Error) => void;
6
6
  };
7
- export declare function AppShell({ directory, stdout, clipboard, onError }: ShellAppProps): import("react").JSX.Element;
7
+ export declare function AppShell({ directory, stdout, clipboard, onError, }: ShellAppProps): import("react").JSX.Element;
8
8
  //# sourceMappingURL=AppShell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AppShell.d.ts","sourceRoot":"","sources":["../../src/ui/AppShell.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAIF,wBAAgB,QAAQ,CAAC,EACtB,SAAS,EACT,MAAM,EACN,SAAS,EACT,OAAO,EACT,EAAE,aAAa,+BAYf"}
1
+ {"version":3,"file":"AppShell.d.ts","sourceRoot":"","sources":["../../src/ui/AppShell.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAIF,wBAAgB,QAAQ,CAAC,EACvB,SAAS,EACT,MAAM,EACN,SAAS,EACT,OAAO,GACR,EAAE,aAAa,+BAYf"}
@@ -4,7 +4,7 @@ import { LoadingApp } from './LoadingApp.js';
4
4
  import { useMemo } from 'react';
5
5
  // App Shell is for choosing the active app, then loading the api,
6
6
  // and showing the app.
7
- export function AppShell({ directory, stdout, clipboard, onError }) {
7
+ export function AppShell({ directory, stdout, clipboard, onError, }) {
8
8
  // Api owns directory watcher and subscriptions
9
9
  const posixAppApi = useMemo(() => loadPosixAppApi(directory), [directory]);
10
10
  return (_jsx(LoadingApp, { appApi: posixAppApi, ...(stdout === undefined ? {} : { stdout }), ...(clipboard === undefined ? {} : { clipboard }), ...(onError === undefined ? {} : { onError }) }));
@@ -1,11 +1,11 @@
1
1
  import { SerializableKey, TreeNode } from 'dirvi-lib';
2
2
  import { AppApi } from '../domain/app-api.js';
3
- type LoadingAppProps<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = {
4
- appApi: AppApi<Id, BufferNode>;
3
+ type LoadingAppProps<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>, ViewKey = string> = {
4
+ appApi: AppApi<Id, BufferNode, ViewKey>;
5
5
  stdout?: (message: string) => void;
6
6
  clipboard?: (value: string) => void;
7
7
  onError?: (error: Error) => void;
8
8
  };
9
- export declare function LoadingApp<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>({ appApi, stdout, clipboard, onError }: LoadingAppProps<Id, BufferNode>): import("react").JSX.Element;
9
+ export declare function LoadingApp<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>, ViewKey = string>({ appApi, stdout, clipboard, onError, }: LoadingAppProps<Id, BufferNode, ViewKey>): import("react").JSX.Element;
10
10
  export {};
11
11
  //# sourceMappingURL=LoadingApp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LoadingApp.d.ts","sourceRoot":"","sources":["../../src/ui/LoadingApp.tsx"],"names":[],"mappings":"AAGA,OAAO,EAEL,eAAe,EAEf,QAAQ,EACT,MAAM,WAAW,CAAC;AAGnB,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,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,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,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,eAAe,CAAC,EAAE,EAAE,UAAU,CAAC,+BAgGxE"}
1
+ {"version":3,"file":"LoadingApp.d.ts","sourceRoot":"","sources":["../../src/ui/LoadingApp.tsx"],"names":[],"mappings":"AAGA,OAAO,EAEL,eAAe,EAEf,QAAQ,EACT,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAI9C,KAAK,eAAe,CAClB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,OAAO,GAAG,MAAM,IACd;IACF,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;IACxC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,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,OAAO,GAAG,MAAM,EAChB,EACA,MAAM,EACN,MAAM,EACN,SAAS,EACT,OAAO,GACR,EAAE,eAAe,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,+BAgG1C"}
@@ -19,7 +19,7 @@ function createInitialState(rootBranches, createEmptyFoldRoot) {
19
19
  },
20
20
  };
21
21
  }
22
- export function LoadingApp({ appApi, stdout, clipboard, onError }) {
22
+ export function LoadingApp({ appApi, stdout, clipboard, onError, }) {
23
23
  const [initialState, setInitialState] = useState();
24
24
  const [empty, setEmpty] = useState(false);
25
25
  const [error, setError] = useState();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mak-98/dirvi-cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "A Vim-inspired TUI for browsing directories.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -13,9 +13,11 @@
13
13
  ],
14
14
  "dependencies": {
15
15
  "commander": "^13.0.0",
16
- "dirvi-lib": "0.6.0",
16
+ "dirvi-lib": "0.7.0",
17
+ "env-paths": "^4.0.0",
17
18
  "ink": "^7.1.1",
18
- "react": "^19.0.0"
19
+ "react": "^19.0.0",
20
+ "zod": "^4.4.3"
19
21
  },
20
22
  "devDependencies": {
21
23
  "@types/node": "^22.0.0",
@@ -1,12 +0,0 @@
1
- import { SerializableKey, State, TreeNode } from 'dirvi-lib';
2
- export type EventMessage<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = {
3
- type: 'view';
4
- view: State<Id, BufferNode>;
5
- } | {
6
- type: 'displayed-leaves-paths';
7
- paths: Id[][];
8
- } | {
9
- type: 'file';
10
- path: Id[];
11
- };
12
- //# sourceMappingURL=event-message.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"event-message.d.ts","sourceRoot":"","sources":["../../src/domain/event-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE7D,MAAM,MAAM,YAAY,CACtB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,IAEzC;IACE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;CAC7B,GACD;IACE,IAAI,EAAE,wBAAwB,CAAC;IAC/B,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC;CACf,GACD;IACE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,EAAE,EAAE,CAAC;CACZ,CAAC"}
@@ -1,3 +0,0 @@
1
- import type { UnixAbsolutePath, PosixNode } from 'dirvi-lib';
2
- export declare function getDirLazyEntries(address: UnixAbsolutePath): Promise<PosixNode[]>;
3
- //# sourceMappingURL=get-dir-lazy-entries.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"get-dir-lazy-entries.d.ts","sourceRoot":"","sources":["../../src/infrastructure/get-dir-lazy-entries.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG7D,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,SAAS,EAAE,CAAC,CAyCtB"}
@@ -1,33 +0,0 @@
1
- import { readdir, readlink } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { UnixPathSchema, PosixNameSchema } from 'dirvi-lib';
4
- export async function getDirLazyEntries(address) {
5
- const directoryEntries = await readdir(address, {
6
- withFileTypes: true,
7
- });
8
- return Promise.all(directoryEntries.map(async (directoryEntry) => {
9
- const name = PosixNameSchema.parse(directoryEntry.name);
10
- if (directoryEntry.isSymbolicLink()) {
11
- const target = UnixPathSchema.parse(await readlink(join(address, directoryEntry.name)));
12
- return {
13
- kind: 'symlink',
14
- name,
15
- target,
16
- };
17
- }
18
- if (directoryEntry.isDirectory()) {
19
- return {
20
- kind: 'directory',
21
- name,
22
- branches: null, // Directory not expanded.
23
- };
24
- }
25
- if (directoryEntry.isFile()) {
26
- return {
27
- kind: 'file',
28
- name,
29
- };
30
- }
31
- throw new Error(`Unsupported filesystem entry "${join(address, directoryEntry.name)}"`);
32
- }));
33
- }
File without changes