@mak-98/dirvi-app 0.0.0-stage → 0.11.0-beta.13

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 (108) hide show
  1. package/LICENSE +674 -0
  2. package/dist/application/app-api-applicator.d.ts +12 -0
  3. package/dist/application/app-api-applicator.d.ts.map +1 -0
  4. package/dist/application/app-api-applicator.js +1 -0
  5. package/dist/application/app-pack-ref.d.ts +12 -0
  6. package/dist/application/app-pack-ref.d.ts.map +1 -0
  7. package/dist/application/app-pack-ref.js +37 -0
  8. package/dist/application/create-app-api.d.ts +11 -0
  9. package/dist/application/create-app-api.d.ts.map +1 -0
  10. package/dist/application/create-app-api.js +60 -0
  11. package/dist/application/create-app-methods.d.ts +11 -0
  12. package/dist/application/create-app-methods.d.ts.map +1 -0
  13. package/dist/application/create-app-methods.js +60 -0
  14. package/dist/application/create-dirvi-api.d.ts +10 -0
  15. package/dist/application/create-dirvi-api.d.ts.map +1 -0
  16. package/dist/application/create-dirvi-api.js +23 -0
  17. package/dist/application/init-runtime.d.ts +40 -0
  18. package/dist/application/init-runtime.d.ts.map +1 -0
  19. package/dist/application/init-runtime.error.d.ts +53 -0
  20. package/dist/application/init-runtime.error.d.ts.map +1 -0
  21. package/dist/application/init-runtime.error.js +77 -0
  22. package/dist/application/init-runtime.js +165 -0
  23. package/dist/application/package-loader.d.ts +9 -0
  24. package/dist/application/package-loader.d.ts.map +1 -0
  25. package/dist/application/package-loader.js +1 -0
  26. package/dist/application/shell-api.d.ts +5 -0
  27. package/dist/application/shell-api.d.ts.map +1 -0
  28. package/dist/application/shell-api.js +1 -0
  29. package/dist/domain/app-input.d.ts +2 -0
  30. package/dist/domain/app-input.d.ts.map +1 -0
  31. package/dist/domain/app-input.js +1 -0
  32. package/dist/domain/app-pack-ref.d.ts +17 -0
  33. package/dist/domain/app-pack-ref.d.ts.map +1 -0
  34. package/dist/domain/app-pack-ref.js +37 -0
  35. package/dist/domain/app.d.ts +39 -0
  36. package/dist/domain/app.d.ts.map +1 -0
  37. package/dist/domain/app.js +25 -0
  38. package/dist/domain/config.d.ts +386 -0
  39. package/dist/domain/config.d.ts.map +1 -0
  40. package/dist/domain/config.js +44 -0
  41. package/dist/domain/dirvi-api/app-api.d.ts +7 -0
  42. package/dist/domain/dirvi-api/app-api.d.ts.map +1 -0
  43. package/dist/domain/dirvi-api/app-api.js +1 -0
  44. package/dist/domain/dirvi-api/dirvi-api.d.ts +17 -0
  45. package/dist/domain/dirvi-api/dirvi-api.d.ts.map +1 -0
  46. package/dist/domain/dirvi-api/dirvi-api.js +1 -0
  47. package/dist/domain/dirvi-api/shell-api.d.ts +35 -0
  48. package/dist/domain/dirvi-api/shell-api.d.ts.map +1 -0
  49. package/dist/domain/dirvi-api/shell-api.js +1 -0
  50. package/dist/domain/dirvi-api.d.ts +24 -0
  51. package/dist/domain/dirvi-api.d.ts.map +1 -0
  52. package/dist/domain/dirvi-api.js +1 -0
  53. package/dist/domain/ex-command.d.ts +9 -0
  54. package/dist/domain/ex-command.d.ts.map +1 -0
  55. package/dist/domain/ex-command.js +1 -0
  56. package/dist/domain/init-runtime.d.ts +41 -0
  57. package/dist/domain/init-runtime.d.ts.map +1 -0
  58. package/dist/domain/init-runtime.js +1 -0
  59. package/dist/domain/initialization.d.ts +26 -0
  60. package/dist/domain/initialization.d.ts.map +1 -0
  61. package/dist/domain/initialization.js +1 -0
  62. package/dist/domain/node-presentation.d.ts +12 -0
  63. package/dist/domain/node-presentation.d.ts.map +1 -0
  64. package/dist/domain/node-presentation.js +1 -0
  65. package/dist/domain/public-init-api.d.ts +48 -0
  66. package/dist/domain/public-init-api.d.ts.map +1 -0
  67. package/dist/domain/public-init-api.js +1 -0
  68. package/dist/domain/shell-api.d.ts +9 -0
  69. package/dist/domain/shell-api.d.ts.map +1 -0
  70. package/dist/domain/shell-api.js +1 -0
  71. package/dist/domain/theme.d.ts +524 -0
  72. package/dist/domain/theme.d.ts.map +1 -0
  73. package/dist/domain/theme.js +164 -0
  74. package/dist/domain/view-name-schema.d.ts +18 -0
  75. package/dist/domain/view-name-schema.d.ts.map +1 -0
  76. package/dist/domain/view-name-schema.js +68 -0
  77. package/dist/index.d.ts +14 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +6 -0
  80. package/dist/theme.d.ts +536 -0
  81. package/dist/theme.d.ts.map +1 -0
  82. package/dist/theme.js +170 -0
  83. package/dist/ui/App.d.ts +21 -0
  84. package/dist/ui/App.d.ts.map +1 -0
  85. package/dist/ui/App.js +248 -0
  86. package/dist/ui/AppSetup.d.ts +18 -0
  87. package/dist/ui/AppSetup.d.ts.map +1 -0
  88. package/dist/ui/AppSetup.js +60 -0
  89. package/dist/ui/Shell.d.ts +44 -0
  90. package/dist/ui/Shell.d.ts.map +1 -0
  91. package/dist/ui/Shell.js +73 -0
  92. package/dist/ui/app-screen-renderer.d.ts +27 -0
  93. package/dist/ui/app-screen-renderer.d.ts.map +1 -0
  94. package/dist/ui/app-screen-renderer.js +1 -0
  95. package/dist/ui/color-resolver.d.ts +3 -0
  96. package/dist/ui/color-resolver.d.ts.map +1 -0
  97. package/dist/ui/color-resolver.js +61 -0
  98. package/dist/ui/hooks/useView.d.ts +5 -0
  99. package/dist/ui/hooks/useView.d.ts.map +1 -0
  100. package/dist/ui/hooks/useView.js +32 -0
  101. package/dist/ui/loading-screen-renderer.d.ts +3 -0
  102. package/dist/ui/loading-screen-renderer.d.ts.map +1 -0
  103. package/dist/ui/loading-screen-renderer.js +1 -0
  104. package/dist/ui/view.d.ts +28 -0
  105. package/dist/ui/view.d.ts.map +1 -0
  106. package/dist/ui/view.js +131 -0
  107. package/package.json +34 -3
  108. package/README.md +0 -3
package/dist/theme.js ADDED
@@ -0,0 +1,170 @@
1
+ import { z } from 'zod';
2
+ // =============================================================================
3
+ // Terminal colors
4
+ // =============================================================================
5
+ const HexColorSchema = z.string().regex(/^#[0-9a-fA-F]{6}$/, {
6
+ message: 'Expected a #rrggbb color',
7
+ });
8
+ const Ansi256ColorSchema = z.number().int().min(0).max(255);
9
+ /**
10
+ * Named ANSI foreground colors supported by Dirvi themes.
11
+ *
12
+ * Renderers translate these symbolic names into their own color APIs. The CLI
13
+ * renderer currently maps them to terminal colors; another renderer does not
14
+ * need to depend on Chalk to interpret the same theme.
15
+ */
16
+ export const basicColorNames = [
17
+ 'black',
18
+ 'red',
19
+ 'green',
20
+ 'yellow',
21
+ 'blue',
22
+ 'cyan',
23
+ 'magenta',
24
+ 'white',
25
+ // Common aliases for blackBright.
26
+ 'gray',
27
+ 'grey',
28
+ 'blackBright',
29
+ 'redBright',
30
+ 'greenBright',
31
+ 'yellowBright',
32
+ 'blueBright',
33
+ 'cyanBright',
34
+ 'magentaBright',
35
+ 'whiteBright',
36
+ ];
37
+ const BasicColorSchema = z.enum(basicColorNames);
38
+ export const ColorConfigSchema = z
39
+ .object({
40
+ hex: HexColorSchema.optional(),
41
+ ansi256: Ansi256ColorSchema.optional(),
42
+ basic: BasicColorSchema.optional(),
43
+ })
44
+ .refine((value) => value.hex !== undefined ||
45
+ value.ansi256 !== undefined ||
46
+ value.basic !== undefined, {
47
+ message: 'Expected at least one terminal color profile',
48
+ });
49
+ // =============================================================================
50
+ // Highlights
51
+ // =============================================================================
52
+ // Note: these schemas are meant to be non-strict
53
+ export const ForegroundHighlightConfigSchema = z.object({
54
+ fg: ColorConfigSchema,
55
+ });
56
+ export const OptionalForegroundHighlightConfigSchema = z.object({
57
+ fg: ColorConfigSchema.optional(),
58
+ });
59
+ export const BackgroundHighlightConfigSchema = z.object({
60
+ bg: ColorConfigSchema,
61
+ });
62
+ export const ForegroundBackgroundHighlightConfigSchema = z.object({
63
+ fg: ColorConfigSchema,
64
+ bg: ColorConfigSchema,
65
+ });
66
+ // =============================================================================
67
+ // Theme defaults
68
+ // =============================================================================
69
+ const black = {
70
+ hex: '#28282a',
71
+ ansi256: 235,
72
+ basic: 'black',
73
+ };
74
+ const lightGray = {
75
+ hex: '#eaeaec',
76
+ ansi256: 254,
77
+ basic: 'gray',
78
+ };
79
+ const leaf = {
80
+ hex: '#e5e5e5',
81
+ ansi256: 254,
82
+ basic: 'white',
83
+ };
84
+ const branch = {
85
+ hex: '#2472c8',
86
+ ansi256: 26,
87
+ basic: 'blue',
88
+ };
89
+ const folded = {
90
+ hex: '#808080',
91
+ ansi256: 245,
92
+ basic: 'gray',
93
+ };
94
+ const indentGuide = {
95
+ hex: '#808080',
96
+ ansi256: 245,
97
+ basic: 'gray',
98
+ };
99
+ const cursorLine = {
100
+ hex: '#3a3a3c',
101
+ ansi256: 237,
102
+ basic: 'gray',
103
+ };
104
+ const success = {
105
+ hex: '#238c4b',
106
+ ansi256: 28,
107
+ basic: 'green',
108
+ };
109
+ const error = {
110
+ hex: '#be2d2d',
111
+ ansi256: 160,
112
+ basic: 'red',
113
+ };
114
+ const themeDefaults = {
115
+ statusBar: {
116
+ command: { fg: black },
117
+ normal: { fg: black },
118
+ background: { bg: lightGray },
119
+ },
120
+ tree: {
121
+ leaf: { fg: leaf },
122
+ branch: { fg: branch },
123
+ folded: { fg: folded },
124
+ cursorLine: { bg: cursorLine },
125
+ indentGuide: { fg: indentGuide },
126
+ },
127
+ feedbackBar: {
128
+ message: {
129
+ fg: black,
130
+ bg: lightGray,
131
+ },
132
+ success: {
133
+ fg: success,
134
+ bg: lightGray,
135
+ },
136
+ error: {
137
+ fg: error,
138
+ bg: lightGray,
139
+ },
140
+ },
141
+ };
142
+ // =============================================================================
143
+ // Theme schema
144
+ // =============================================================================
145
+ export const ThemeSchema = z.object({
146
+ statusBar: z
147
+ .object({
148
+ command: ForegroundHighlightConfigSchema.default(themeDefaults.statusBar.command),
149
+ normal: ForegroundHighlightConfigSchema.default(themeDefaults.statusBar.normal),
150
+ background: BackgroundHighlightConfigSchema.default(themeDefaults.statusBar.background),
151
+ })
152
+ .default(themeDefaults.statusBar),
153
+ tree: z
154
+ .object({
155
+ leaf: ForegroundHighlightConfigSchema.default(themeDefaults.tree.leaf),
156
+ branch: ForegroundHighlightConfigSchema.default(themeDefaults.tree.branch),
157
+ folded: ForegroundHighlightConfigSchema.default(themeDefaults.tree.folded),
158
+ cursorLine: BackgroundHighlightConfigSchema.default(themeDefaults.tree.cursorLine),
159
+ indentGuide: OptionalForegroundHighlightConfigSchema.default(themeDefaults.tree.indentGuide),
160
+ })
161
+ .default(themeDefaults.tree),
162
+ feedbackBar: z
163
+ .object({
164
+ message: ForegroundBackgroundHighlightConfigSchema.default(themeDefaults.feedbackBar.message),
165
+ success: ForegroundBackgroundHighlightConfigSchema.default(themeDefaults.feedbackBar.success),
166
+ error: ForegroundBackgroundHighlightConfigSchema.default(themeDefaults.feedbackBar.error),
167
+ })
168
+ .default(themeDefaults.feedbackBar),
169
+ });
170
+ export const defaultTheme = ThemeSchema.parse({});
@@ -0,0 +1,21 @@
1
+ import { Config, ShellApi } from 'dirvi-apis';
2
+ import type { AppMethods, EffectToAction, IntentToEffect, Reducer, SerializableKey, State } from 'dirvi-lib';
3
+ import type { AppScreenRenderer } from './app-screen-renderer.js';
4
+ import { ExCommand } from '../domain/ex-command.js';
5
+ type AppProps<Id extends SerializableKey, Value, ViewKey = string> = {
6
+ screenRenderer: AppScreenRenderer;
7
+ shellApi: ShellApi;
8
+ appMethods: AppMethods<Id, Value, ViewKey>;
9
+ exCommands: ReadonlyMap<string, ExCommand>;
10
+ config: Readonly<Config>;
11
+ initialState: State<Id, Value>;
12
+ reducer: Reducer<Id, Value>;
13
+ intentToEffect: IntentToEffect<Id, Value>;
14
+ effectToAction: EffectToAction<Id, Value>;
15
+ stdout?: (message: string) => void;
16
+ clipboard?: (value: string) => void;
17
+ onError?: (error: Error) => void;
18
+ };
19
+ export declare function App<Id extends SerializableKey, Value, ViewKey = string>({ screenRenderer, shellApi, appMethods, exCommands, config, initialState, reducer, intentToEffect, effectToAction, stdout, clipboard, onError, }: AppProps<Id, Value, ViewKey>): import("react").JSX.Element;
20
+ export {};
21
+ //# sourceMappingURL=App.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/ui/App.tsx"],"names":[],"mappings":"AASA,OAAO,EAAU,MAAM,EAAgB,QAAQ,EAAE,MAAM,YAAY,CAAC;AACpE,OAAO,KAAK,EACV,UAAU,EAEV,cAAc,EAEd,cAAc,EAEd,OAAO,EACP,eAAe,EACf,KAAK,EAGN,MAAM,WAAW,CAAC;AAInB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD,KAAK,QAAQ,CAAC,EAAE,SAAS,eAAe,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,IAAI;IACnE,cAAc,EAAE,iBAAiB,CAAC;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IAC3C,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC3C,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzB,YAAY,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IAC/B,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IAC5B,cAAc,EAAE,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IAC1C,cAAc,EAAE,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IAC1C,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,CAAC,EAAE,SAAS,eAAe,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,EACvE,cAAc,EACd,QAAQ,EACR,UAAU,EACV,UAAU,EACV,MAAM,EACN,YAAY,EACZ,OAAO,EACP,cAAc,EACd,cAAc,EACd,MAAM,EACN,SAAS,EACT,OAAO,GACR,EAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,+BAyV9B"}
package/dist/ui/App.js ADDED
@@ -0,0 +1,248 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState, } from 'react';
3
+ import { createFeedbackApi, userInputToIntent } from 'dirvi-lib';
4
+ import { useView } from './hooks/useView.js';
5
+ export function App({ screenRenderer, shellApi, appMethods, exCommands, config, initialState, reducer, intentToEffect, effectToAction, stdout, clipboard, onError, }) {
6
+ const ScreenRenderer = screenRenderer;
7
+ const [state, dispatch] = useReducer(reducer, initialState);
8
+ const stateRef = useRef(state); // Gives `subscribeToResync` callback a way to
9
+ // see current state
10
+ stateRef.current = state;
11
+ const navEntry = useMemo(() => {
12
+ const root = appMethods.stateMethods.getRoot(state);
13
+ const folds = appMethods.stateMethods.getFolds(state);
14
+ const navigation = appMethods.navMethods.from(root, folds);
15
+ if (navigation === undefined) {
16
+ throw new Error('Tree-surfer state root must be a loaded branch.');
17
+ }
18
+ return navigation;
19
+ }, [appMethods.navMethods, appMethods.stateMethods, state]);
20
+ const clearInputState = { inputMode: 'normal', normalBuffer: '' };
21
+ const [inputState, setInputState] = useState(clearInputState);
22
+ const [feedback, setFeedback] = useState();
23
+ const [rowCount, setRowCount] = useState(0);
24
+ const handleRowsChange = useCallback((nextRows) => {
25
+ setRowCount((currentRows) => currentRows === nextRows ? currentRows : nextRows);
26
+ }, []);
27
+ const view = useView(navEntry, state, appMethods.stateMethods, appMethods.navMethods, appMethods.stateMethods.cursorMethods, rowCount, appMethods.presentNode);
28
+ // ===========================================================================
29
+ // Resync Subscriptions
30
+ // ===========================================================================
31
+ useEffect(() => {
32
+ let active = true;
33
+ const unsubscribe = appMethods.subscribeToResync(() => {
34
+ const oldState = stateRef.current;
35
+ void appMethods.stateMethods
36
+ .resync(oldState, appMethods.loadChildren)
37
+ .then((newState) => {
38
+ if (!active) {
39
+ return;
40
+ }
41
+ dispatch({
42
+ kind: 'setState',
43
+ oldState: oldState,
44
+ newState: newState,
45
+ });
46
+ })
47
+ .catch((error) => {
48
+ const appError = error instanceof Error ? error : new Error(String(error));
49
+ onError?.(appError);
50
+ });
51
+ });
52
+ return () => {
53
+ active = false;
54
+ unsubscribe();
55
+ };
56
+ }, [appMethods, onError]);
57
+ // ===========================================================================
58
+ // App API
59
+ // ===========================================================================
60
+ const feedbackApi = useMemo(() => createFeedbackApi((nextFeedback) => {
61
+ setFeedback(nextFeedback);
62
+ }), []);
63
+ const appApi = useMemo(() => ({
64
+ pathAtCursor: () => appMethods.stateMethods.cursorPath(stateRef.current),
65
+ visiblePaths: () => {
66
+ const state = stateRef.current;
67
+ const root = appMethods.stateMethods.getRoot(state);
68
+ const folds = appMethods.stateMethods.getFolds(state);
69
+ const navigation = appMethods.navMethods.from(root, folds);
70
+ if (navigation === undefined) {
71
+ throw new Error('Tree-surfer state root must be a loaded branch.');
72
+ }
73
+ return navigation.children === null
74
+ ? []
75
+ : appMethods.navMethods.visiblePaths(navigation.children);
76
+ },
77
+ // TODO: Implement with appMethods
78
+ valueAtCursor: () => 'test value',
79
+ addMessage: (message, type) => {
80
+ feedbackApi.addMessage(message, type);
81
+ },
82
+ }), [feedbackApi, appMethods.stateMethods, appMethods.navMethods]);
83
+ // ===========================================================================
84
+ // Executing Effects
85
+ // ===========================================================================
86
+ const executeEffect = useCallback((effect) => {
87
+ if (effect === undefined) {
88
+ return;
89
+ }
90
+ switch (effect.effectType) {
91
+ /** Execute a command, using shell API and app API. */
92
+ case 'userExCommand': {
93
+ setInputState(clearInputState);
94
+ const command = exCommands.get(effect.name);
95
+ if (command === undefined) {
96
+ appApi.addMessage(`Command is no longer registered: ${effect.name}`, 'error');
97
+ return;
98
+ }
99
+ void Promise.resolve()
100
+ .then(() => command.execute({ shell: shellApi, app: appApi }, effect.args))
101
+ .catch((error) => {
102
+ const appError = error instanceof Error ? error : new Error(String(error));
103
+ appApi.addMessage(`Command "${effect.name}" failed: ${appError.message}`, 'error');
104
+ });
105
+ return;
106
+ }
107
+ case 'quit':
108
+ shellApi.quit(effect.exitMessage);
109
+ return;
110
+ case 'listApps': {
111
+ const appIds = shellApi.listApps();
112
+ setInputState(clearInputState);
113
+ appApi.addMessage(['Apps:', ...appIds].join('\n'));
114
+ return;
115
+ }
116
+ case 'switchApp':
117
+ void shellApi.loadApp(effect.appId, effect.input).then((result) => {
118
+ if (result.kind !== 'loaded') {
119
+ appApi.addMessage(result.message, 'error');
120
+ }
121
+ });
122
+ return;
123
+ case 'dispatchEffectAction':
124
+ const effectActionResult = effectToAction(effect.action, navEntry, state);
125
+ setInputState(clearInputState);
126
+ switch (effectActionResult.kind) {
127
+ case 'action':
128
+ dispatch(effectActionResult.action);
129
+ return;
130
+ case 'rejected':
131
+ appApi.addMessage(effectActionResult.message, 'error');
132
+ return;
133
+ case 'ignored':
134
+ return;
135
+ default:
136
+ return;
137
+ }
138
+ case 'setInputState':
139
+ setInputState(effect.inputState);
140
+ return;
141
+ case 'loadBranchEntries': {
142
+ void appMethods
143
+ .loadChildren(effect.path)
144
+ .then((entries) => {
145
+ dispatch({
146
+ kind: 'updateBranch',
147
+ path: effect.path,
148
+ entries,
149
+ });
150
+ })
151
+ .catch((error) => {
152
+ const appError = error instanceof Error ? error : new Error(String(error));
153
+ appApi.addMessage(`Unable to open directory: ${appError.message}`);
154
+ });
155
+ return;
156
+ }
157
+ case 'saveView': {
158
+ // void appApi.viewApi
159
+ // .save(appApi.viewKey, effect.name, stateRef.current)
160
+ // .then(() => {
161
+ // setInputState({
162
+ // inputMode: 'normal',
163
+ // normalBuffer: '',
164
+ // });
165
+ // feedbackApi.addMessage(`Saved view: ${effect.name}`);
166
+ // })
167
+ // .catch((error: unknown) => {
168
+ // feedbackApi.addMessage(
169
+ // `Unable to save view: ${toError(error).message}`,
170
+ // );
171
+ // });
172
+ return;
173
+ }
174
+ case 'loadView': {
175
+ // void appApi.viewApi
176
+ // .load(appApi.viewKey, effect.name)
177
+ // .then((loadedState) => {
178
+ // if (loadedState === undefined) {
179
+ // throw new Error(`View not found: ${effect.name}`);
180
+ // }
181
+ //
182
+ // // The saved buffer may be stale because the filesystem could have
183
+ // // changed since the view was saved.
184
+ // return appApi.stateApi.resync(loadedState, appApi.loadBranches);
185
+ // })
186
+ // .then((newState) => {
187
+ // const oldState = stateRef.current;
188
+ //
189
+ // dispatch({
190
+ // kind: 'setState',
191
+ // oldState,
192
+ // newState,
193
+ // });
194
+ //
195
+ // setInputState({
196
+ // inputMode: 'normal',
197
+ // normalBuffer: '',
198
+ // });
199
+ // })
200
+ // .catch((error: unknown) => {
201
+ // onError?.(toError(error));
202
+ // });
203
+ return;
204
+ }
205
+ case 'listFolds': {
206
+ const folds = appMethods.stateMethods.getFolds(state);
207
+ const foldTrees = [
208
+ ...appMethods.stateMethods.foldMethods.indexes(folds),
209
+ ].map((index) => ({
210
+ index,
211
+ info: appMethods.stateMethods.foldMethods.getInfoAtIndex(folds, index),
212
+ active: appMethods.stateMethods.foldMethods.isActiveAtIndex(folds, index),
213
+ }));
214
+ const indexWidth = Math.max(...foldTrees.map(({ index }) => String(index).length));
215
+ const lines = foldTrees.map(({ index, info, active }) => {
216
+ const status = active === true ? 'active' : 'inactive';
217
+ const name = info?.name ?? '<unavailable>';
218
+ return `${String(index).padStart(indexWidth)} ${status.padEnd(8)} ${name}`;
219
+ });
220
+ setInputState(clearInputState);
221
+ appApi.addMessage(['Fold trees:', ...lines].join('\n'));
222
+ return;
223
+ }
224
+ case 'emitError':
225
+ appApi.addMessage(effect.message, 'error');
226
+ return;
227
+ }
228
+ }, [appMethods, effectToAction, navEntry, state, shellApi, appApi, clearInputState, exCommands]);
229
+ // ===========================================================================
230
+ // Input hook
231
+ // ===========================================================================
232
+ const handleInput = useCallback((userInput) => {
233
+ const intent = userInputToIntent(userInput, inputState);
234
+ if (intent === undefined) {
235
+ return;
236
+ }
237
+ const effectResult = intentToEffect(intent, state);
238
+ if (effectResult === undefined) {
239
+ return;
240
+ }
241
+ setFeedback(undefined);
242
+ executeEffect(effectResult);
243
+ }, [executeEffect, inputState, intentToEffect, state]);
244
+ // ===========================================================================
245
+ // Render
246
+ // ===========================================================================
247
+ return (_jsx(ScreenRenderer, { view: view, feedback: feedback, feedbackApi: feedbackApi, config: config, inputState: inputState, handleInput: handleInput, onRowsChange: handleRowsChange }));
248
+ }
@@ -0,0 +1,18 @@
1
+ import { AppMethods, SerializableKey } from 'dirvi-lib';
2
+ import type { LoadingScreenRenderer } from './loading-screen-renderer.js';
3
+ import type { AppScreenRenderer } from './app-screen-renderer.js';
4
+ import type { Initialization } from '../domain/initialization.js';
5
+ import type { ShellApi } from 'dirvi-apis';
6
+ type AppSetupProps<Id extends SerializableKey, Value, ViewKey = string> = {
7
+ appScreenRenderer: AppScreenRenderer;
8
+ loadingScreenRenderer: LoadingScreenRenderer;
9
+ appId: string;
10
+ initialization: Initialization;
11
+ shellApi: ShellApi;
12
+ appMethods: AppMethods<Id, Value, ViewKey>;
13
+ stdout?: (message: string) => void;
14
+ clipboard?: (value: string) => void;
15
+ };
16
+ export declare function AppSetup<Id extends SerializableKey, Value, ViewKey = string>({ appScreenRenderer, loadingScreenRenderer, appId, initialization, shellApi, appMethods, stdout, clipboard, }: AppSetupProps<Id, Value, ViewKey>): import("react").JSX.Element;
17
+ export {};
18
+ //# sourceMappingURL=AppSetup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppSetup.d.ts","sourceRoot":"","sources":["../../src/ui/AppSetup.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAqB,eAAe,EAAS,MAAM,WAAW,CAAC;AAOlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAO3C,KAAK,aAAa,CAAC,EAAE,SAAS,eAAe,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,IAAI;IACxE,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,qBAAqB,EAAE,qBAAqB,CAAC;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IAC3C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAkBF,wBAAgB,QAAQ,CAAC,EAAE,SAAS,eAAe,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,EAC5E,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,EACL,cAAc,EACd,QAAQ,EACR,UAAU,EACV,MAAM,EACN,SAAS,GACV,EAAE,aAAa,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,+BAwGnC"}
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { createTreeMethods } from 'dirvi-lib';
4
+ import { createEffectToAction, createIntentToEffect, createReducer, } from 'dirvi-lib';
5
+ import { App } from './App.js';
6
+ // TODO Later: configuration watched for changes
7
+ const emptyExCommands = new Map();
8
+ export function AppSetup({ appScreenRenderer, loadingScreenRenderer, appId, initialization, shellApi, appMethods, stdout, clipboard, }) {
9
+ const [loadState, setLoadState] = useState();
10
+ useEffect(() => {
11
+ let mounted = true;
12
+ setLoadState({ status: 'loading', appMethods: appMethods });
13
+ async function initialize() {
14
+ try {
15
+ const root = await appMethods.loadRoot();
16
+ if (!mounted) {
17
+ return;
18
+ }
19
+ const rootId = createTreeMethods().id(root);
20
+ const folds = appMethods.stateMethods.foldMethods.create(rootId, {
21
+ name: 'default',
22
+ description: null,
23
+ });
24
+ const initialState = appMethods.stateMethods.create(root, folds, { path: [] });
25
+ if (initialState === undefined) {
26
+ throw new Error('Cannot create initial app state');
27
+ }
28
+ setLoadState({ status: 'ready', appMethods: appMethods, initialState });
29
+ }
30
+ catch (cause) {
31
+ if (!mounted) {
32
+ return;
33
+ }
34
+ const error = cause instanceof Error ? cause : new Error(String(cause));
35
+ setLoadState({ status: 'error', appMethods: appMethods, error });
36
+ }
37
+ }
38
+ void initialize();
39
+ return () => {
40
+ mounted = false;
41
+ };
42
+ }, [appMethods]);
43
+ // --- Pure function deps ---
44
+ const exCommands = useMemo(() => initialization.exCommands.get(appId) ?? emptyExCommands, [initialization.exCommands, appId]);
45
+ const reducer = useMemo(() => createReducer(appMethods.stateMethods, appMethods.navMethods), [appMethods.stateMethods, appMethods.navMethods]);
46
+ const intentToEffect = useMemo(() => createIntentToEffect(new Set(exCommands.keys()), appMethods.stateMethods), [appMethods.stateMethods, exCommands]);
47
+ const effectToAction = useMemo(() => createEffectToAction(appMethods.stateMethods, appMethods.navMethods), [appMethods.navMethods, appMethods.stateMethods]);
48
+ // --- JSX ---
49
+ const LoadingScreenRenderer = loadingScreenRenderer;
50
+ if (loadState === undefined || loadState.appMethods !== appMethods) {
51
+ return _jsx(LoadingScreenRenderer, {});
52
+ }
53
+ if (loadState.status === 'loading') {
54
+ return _jsx(LoadingScreenRenderer, {});
55
+ }
56
+ if (loadState.status === 'error') {
57
+ throw loadState.error;
58
+ }
59
+ return (_jsx(App, { screenRenderer: appScreenRenderer, shellApi: shellApi, appMethods: appMethods, exCommands: exCommands, config: initialization.config, initialState: loadState.initialState, reducer: reducer, intentToEffect: intentToEffect, effectToAction: effectToAction, ...(stdout === undefined ? {} : { stdout }), ...(clipboard === undefined ? {} : { clipboard }) }));
60
+ }
@@ -0,0 +1,44 @@
1
+ import type { AppScreenRenderer } from './app-screen-renderer.js';
2
+ import type { LoadingScreenRenderer } from './loading-screen-renderer.js';
3
+ import type { Initialization } from '../domain/initialization.js';
4
+ import type { PackageLoader } from '../application/package-loader.js';
5
+ export type InitialAppOptions = Readonly<{
6
+ /**
7
+ * Requested app ID at startup.
8
+ *
9
+ * The Shell will later use this to override or select the app chosen by
10
+ * initialization.
11
+ */
12
+ readonly app?: string;
13
+ /**
14
+ * Host-provided startup input interpreted by the selected app.
15
+ */
16
+ readonly input?: string;
17
+ }>;
18
+ /**
19
+ * Dependencies and output callbacks required by {@link Shell}.
20
+ *
21
+ * The API loader functions are injected so the shell does not depend directly
22
+ * on a platform-specific infrastructure implementation.
23
+ */
24
+ export type ShellAppProps = {
25
+ /**
26
+ * Host-specific React implementation.
27
+ */
28
+ readonly loadingScreenRenderer: LoadingScreenRenderer;
29
+ readonly appScreenRenderer: AppScreenRenderer;
30
+ readonly initialAppOptions: InitialAppOptions;
31
+ readonly initialization: Initialization;
32
+ readonly packageLoader: PackageLoader;
33
+ /** Request to exit dirvi with an optional message. */
34
+ readonly onQuit: (message?: string) => void;
35
+ /** Writes non-interactive application output. */
36
+ readonly stdout?: (message: string) => void;
37
+ /** Copies a value to the platform clipboard. */
38
+ readonly clipboard?: (value: string) => void;
39
+ };
40
+ /**
41
+ * Creates `ShellApi` and the `AppMethods` for a selected application.
42
+ */
43
+ export declare function Shell({ onQuit, loadingScreenRenderer, appScreenRenderer, initialAppOptions, initialization, packageLoader, stdout, clipboard, }: ShellAppProps): import("react").JSX.Element;
44
+ //# sourceMappingURL=Shell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Shell.d.ts","sourceRoot":"","sources":["../../src/ui/Shell.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAItE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B;;OAEG;IACH,QAAQ,CAAC,qBAAqB,EAAE,qBAAqB,CAAC;IACtD,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;IAE9C,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;IAC9C,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IAEtC,sDAAsD;IACtD,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,iDAAiD;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,gDAAgD;IAChD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,CAAC;AAEF;;GAEG;AACH,wBAAgB,KAAK,CAAC,EACpB,MAAM,EACN,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,MAAM,EACN,SAAS,GACV,EAAE,aAAa,+BA6Ff"}
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { AppSetup } from './AppSetup.js';
4
+ import { createAppMethodsApplicator } from '../application/create-app-methods.js';
5
+ /**
6
+ * Creates `ShellApi` and the `AppMethods` for a selected application.
7
+ */
8
+ export function Shell({ onQuit, loadingScreenRenderer, appScreenRenderer, initialAppOptions, initialization, packageLoader, stdout, clipboard, }) {
9
+ const [loadedApp, setLoadedApp] = useState();
10
+ const [appError, setAppError] = useState(undefined);
11
+ /**
12
+ * Create the Shell API, based on given {@link Initialization}.
13
+ */
14
+ const shellApi = useMemo(() => {
15
+ return {
16
+ listApps: () => [...initialization.app.registered.keys()],
17
+ loadApp: async (appId, input) => {
18
+ try {
19
+ const appReference = initialization.app.registered.get(appId);
20
+ if (appReference === undefined) {
21
+ return {
22
+ kind: 'notRegistered',
23
+ appId,
24
+ message: `App "${appId}" has not been registered in initial configuration script.`,
25
+ };
26
+ }
27
+ const appPlugin = await packageLoader.loadAppPlugin(appReference);
28
+ const loadedAppMethodsApplicator = createAppMethodsApplicator(appPlugin.createAppDefinitionApplicator(input));
29
+ setLoadedApp({
30
+ appId,
31
+ appMethodsApplicator: loadedAppMethodsApplicator,
32
+ });
33
+ return { kind: 'loaded' };
34
+ }
35
+ catch (error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ return {
38
+ kind: 'loadFailed',
39
+ appId,
40
+ message: `Unable to load app "${appId}": ${message}`,
41
+ };
42
+ }
43
+ },
44
+ quit: onQuit,
45
+ };
46
+ }, [packageLoader, onQuit, initialization.app.registered]);
47
+ /**
48
+ * Load initial `AppMethods` on startup, using the shell API just created and
49
+ * {@link Initialization}.
50
+ */
51
+ useEffect(() => {
52
+ void shellApi
53
+ .loadApp(initialAppOptions.app ?? initialization.app.initial, initialAppOptions.input)
54
+ .then((result) => {
55
+ if (result.kind !== 'loaded') {
56
+ setAppError(new Error(result.message));
57
+ }
58
+ });
59
+ }, [
60
+ initialAppOptions,
61
+ initialization.app.initial,
62
+ shellApi,
63
+ ]);
64
+ if (appError !== undefined) {
65
+ throw appError;
66
+ }
67
+ // TODO Later: Loading messages
68
+ if (loadedApp === undefined) {
69
+ const LoadingScreenRenderer = loadingScreenRenderer;
70
+ return _jsx(LoadingScreenRenderer, {});
71
+ }
72
+ return loadedApp.appMethodsApplicator((appMethods) => (_jsx(AppSetup, { appScreenRenderer: appScreenRenderer, loadingScreenRenderer: loadingScreenRenderer, appId: loadedApp.appId, initialization: initialization, shellApi: shellApi, appMethods: appMethods, ...(stdout === undefined ? {} : { stdout }), ...(clipboard === undefined ? {} : { clipboard }) })));
73
+ }