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

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 (81) hide show
  1. package/LICENSE +674 -0
  2. package/dist/application/create-app-api.d.ts +11 -0
  3. package/dist/application/create-app-api.d.ts.map +1 -0
  4. package/dist/application/create-app-api.js +33 -0
  5. package/dist/application/create-dirvi-api.d.ts +10 -0
  6. package/dist/application/create-dirvi-api.d.ts.map +1 -0
  7. package/dist/application/create-dirvi-api.js +23 -0
  8. package/dist/application/init-runtime.d.ts +20 -0
  9. package/dist/application/init-runtime.d.ts.map +1 -0
  10. package/dist/application/init-runtime.js +117 -0
  11. package/dist/application/package-loader.d.ts +9 -0
  12. package/dist/application/package-loader.d.ts.map +1 -0
  13. package/dist/application/package-loader.js +1 -0
  14. package/dist/application/shell-api.d.ts +5 -0
  15. package/dist/application/shell-api.d.ts.map +1 -0
  16. package/dist/application/shell-api.js +1 -0
  17. package/dist/domain/app-pack-ref.d.ts +17 -0
  18. package/dist/domain/app-pack-ref.d.ts.map +1 -0
  19. package/dist/domain/app-pack-ref.js +37 -0
  20. package/dist/domain/app.d.ts +39 -0
  21. package/dist/domain/app.d.ts.map +1 -0
  22. package/dist/domain/app.js +25 -0
  23. package/dist/domain/config.d.ts +402 -0
  24. package/dist/domain/config.d.ts.map +1 -0
  25. package/dist/domain/config.js +44 -0
  26. package/dist/domain/dirvi-api.d.ts +24 -0
  27. package/dist/domain/dirvi-api.d.ts.map +1 -0
  28. package/dist/domain/dirvi-api.js +1 -0
  29. package/dist/domain/ex-command.d.ts +12 -0
  30. package/dist/domain/ex-command.d.ts.map +1 -0
  31. package/dist/domain/ex-command.js +1 -0
  32. package/dist/domain/init-runtime.d.ts +41 -0
  33. package/dist/domain/init-runtime.d.ts.map +1 -0
  34. package/dist/domain/init-runtime.js +1 -0
  35. package/dist/domain/initialization.d.ts +20 -0
  36. package/dist/domain/initialization.d.ts.map +1 -0
  37. package/dist/domain/initialization.js +1 -0
  38. package/dist/domain/public-init-api.d.ts +42 -0
  39. package/dist/domain/public-init-api.d.ts.map +1 -0
  40. package/dist/domain/public-init-api.js +1 -0
  41. package/dist/domain/shell-api.d.ts +5 -0
  42. package/dist/domain/shell-api.d.ts.map +1 -0
  43. package/dist/domain/shell-api.js +1 -0
  44. package/dist/domain/theme.d.ts +536 -0
  45. package/dist/domain/theme.d.ts.map +1 -0
  46. package/dist/domain/theme.js +170 -0
  47. package/dist/domain/view-name-schema.d.ts +18 -0
  48. package/dist/domain/view-name-schema.d.ts.map +1 -0
  49. package/dist/domain/view-name-schema.js +68 -0
  50. package/dist/index.d.ts +19 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +7 -0
  53. package/dist/theme.d.ts +536 -0
  54. package/dist/theme.d.ts.map +1 -0
  55. package/dist/theme.js +170 -0
  56. package/dist/ui/App.d.ts +18 -0
  57. package/dist/ui/App.d.ts.map +1 -0
  58. package/dist/ui/App.js +225 -0
  59. package/dist/ui/AppSetup.d.ts +17 -0
  60. package/dist/ui/AppSetup.d.ts.map +1 -0
  61. package/dist/ui/AppSetup.js +88 -0
  62. package/dist/ui/Shell.d.ts +49 -0
  63. package/dist/ui/Shell.d.ts.map +1 -0
  64. package/dist/ui/Shell.js +54 -0
  65. package/dist/ui/app-screen-renderer.d.ts +28 -0
  66. package/dist/ui/app-screen-renderer.d.ts.map +1 -0
  67. package/dist/ui/app-screen-renderer.js +1 -0
  68. package/dist/ui/color-resolver.d.ts +3 -0
  69. package/dist/ui/color-resolver.d.ts.map +1 -0
  70. package/dist/ui/color-resolver.js +61 -0
  71. package/dist/ui/hooks/useView.d.ts +4 -0
  72. package/dist/ui/hooks/useView.d.ts.map +1 -0
  73. package/dist/ui/hooks/useView.js +32 -0
  74. package/dist/ui/loading-screen-renderer.d.ts +3 -0
  75. package/dist/ui/loading-screen-renderer.d.ts.map +1 -0
  76. package/dist/ui/loading-screen-renderer.js +1 -0
  77. package/dist/ui/view.d.ts +23 -0
  78. package/dist/ui/view.d.ts.map +1 -0
  79. package/dist/ui/view.js +74 -0
  80. package/package.json +34 -3
  81. 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,18 @@
1
+ import type { AppApi, EffectToAction, IntentToEffect, Reducer, SerializableKey, State } from 'dirvi-lib';
2
+ import type { Config } from '../domain/config.js';
3
+ import type { AppScreenRenderer } from './app-screen-renderer.js';
4
+ type AppProps<Id extends SerializableKey, Value, ViewKey = string> = {
5
+ screenRenderer: AppScreenRenderer;
6
+ appApi: AppApi<Id, Value, ViewKey>;
7
+ config: Config;
8
+ initialState: State<Id, Value>;
9
+ reducer: Reducer<Id, Value>;
10
+ intentToEffect: IntentToEffect<Id, Value>;
11
+ effectToAction: EffectToAction<Id, Value>;
12
+ stdout?: (message: string) => void;
13
+ clipboard?: (value: string) => void;
14
+ onError?: (error: Error) => void;
15
+ };
16
+ export declare function App<Id extends SerializableKey, Value, ViewKey = string>({ screenRenderer, appApi, config, initialState, reducer, intentToEffect, effectToAction, stdout, clipboard, onError, }: AppProps<Id, Value, ViewKey>): import("react").JSX.Element;
17
+ export {};
18
+ //# sourceMappingURL=App.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/ui/App.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,MAAM,EAEN,cAAc,EAEd,cAAc,EAEd,OAAO,EACP,eAAe,EACf,KAAK,EAGN,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAMlE,KAAK,QAAQ,CAAC,EAAE,SAAS,eAAe,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,IAAI;IACnE,cAAc,EAAE,iBAAiB,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,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,MAAM,EACN,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,+BAkT9B"}
package/dist/ui/App.js ADDED
@@ -0,0 +1,225 @@
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
+ function toError(error) {
6
+ return error instanceof Error ? error : new Error(String(error));
7
+ }
8
+ export function App({ screenRenderer, appApi, config, initialState, reducer, intentToEffect, effectToAction, stdout, clipboard, onError, }) {
9
+ const ScreenRenderer = screenRenderer;
10
+ const [state, dispatch] = useReducer(reducer, initialState);
11
+ const stateRef = useRef(state); // Gives `subscribeToResync` callback a way to
12
+ // see current state
13
+ stateRef.current = state;
14
+ const navEntry = useMemo(() => {
15
+ const navigation = appApi.navNodeApi.from(state.root, state.folds);
16
+ if (navigation === undefined) {
17
+ throw new Error('Tree-surfer state root must be a loaded branch.');
18
+ }
19
+ return navigation;
20
+ }, [appApi.navNodeApi, state.root, state.folds]);
21
+ const [inputState, setInputState] = useState({
22
+ inputMode: 'normal',
23
+ normalBuffer: '',
24
+ });
25
+ const [feedback, setFeedback] = useState();
26
+ const [exitStatus, setExitStatus] = useState();
27
+ const [rowCount, setRowCount] = useState(0);
28
+ const handleRowsChange = useCallback((nextRows) => {
29
+ setRowCount((currentRows) => currentRows === nextRows ? currentRows : nextRows);
30
+ }, []);
31
+ // ===========================================================================
32
+ // Display hooks
33
+ // ===========================================================================
34
+ const feedbackApi = useMemo(() => createFeedbackApi((nextFeedback) => {
35
+ setFeedback(nextFeedback);
36
+ }), []);
37
+ const view = useView(navEntry, state, appApi.navNodeApi, appApi.cursorApi, rowCount);
38
+ // ===========================================================================
39
+ // Resync Subscriptions
40
+ // ===========================================================================
41
+ useEffect(() => {
42
+ let active = true;
43
+ const unsubscribe = appApi.subscribeToResync(() => {
44
+ const oldState = stateRef.current;
45
+ void appApi.stateApi
46
+ .resync(oldState, appApi.loadBranches)
47
+ .then((newState) => {
48
+ if (!active) {
49
+ return;
50
+ }
51
+ dispatch({
52
+ kind: 'setState',
53
+ oldState: oldState,
54
+ newState: newState,
55
+ });
56
+ })
57
+ .catch((error) => {
58
+ const appError = error instanceof Error ? error : new Error(String(error));
59
+ onError?.(appError);
60
+ });
61
+ });
62
+ return () => {
63
+ active = false;
64
+ unsubscribe();
65
+ };
66
+ }, [appApi, onError]);
67
+ // ===========================================================================
68
+ // Executing Effects
69
+ // ===========================================================================
70
+ const executeEffect = useCallback((effect) => {
71
+ if (effect === undefined) {
72
+ return;
73
+ }
74
+ switch (effect.effectType) {
75
+ case 'dispatchEffectAction':
76
+ const effectActionResult = effectToAction(effect.action, navEntry, state);
77
+ // A completed normal-mode command or submitted Ex command always
78
+ // leaves its input buffer, whether it succeeds, is rejected, or no-ops.
79
+ setInputState({
80
+ inputMode: 'normal',
81
+ normalBuffer: '',
82
+ });
83
+ switch (effectActionResult.kind) {
84
+ case 'action':
85
+ dispatch(effectActionResult.action);
86
+ return;
87
+ case 'rejected':
88
+ feedbackApi.addMessage(effectActionResult.message, 'error');
89
+ return;
90
+ case 'ignored':
91
+ return;
92
+ default:
93
+ return;
94
+ }
95
+ case 'setInputState':
96
+ setInputState(effect.inputState);
97
+ return;
98
+ case 'loadBranchEntries': {
99
+ void appApi
100
+ .loadBranches(effect.path)
101
+ .then((entries) => {
102
+ dispatch({
103
+ kind: 'updateBranch',
104
+ path: effect.path,
105
+ entries,
106
+ });
107
+ })
108
+ .catch((error) => {
109
+ const appError = error instanceof Error ? error : new Error(String(error));
110
+ feedbackApi.addMessage(`Unable to open directory: ${appError.message}`);
111
+ });
112
+ return;
113
+ }
114
+ case 'emitVisibleLeavesPaths':
115
+ const navigation = navEntry.children;
116
+ if (navigation === null) {
117
+ return;
118
+ }
119
+ const visibleLeavesPaths = appApi.navNodeApi.visibleLeavesPaths(navigation);
120
+ stdout?.(JSON.stringify({
121
+ type: 'displayed-leaves-paths',
122
+ paths: visibleLeavesPaths,
123
+ }));
124
+ setInputState({
125
+ inputMode: 'normal',
126
+ normalBuffer: '',
127
+ });
128
+ return;
129
+ case 'saveView': {
130
+ // void appApi.viewApi
131
+ // .save(appApi.viewKey, effect.name, stateRef.current)
132
+ // .then(() => {
133
+ // setInputState({
134
+ // inputMode: 'normal',
135
+ // normalBuffer: '',
136
+ // });
137
+ // feedbackApi.addMessage(`Saved view: ${effect.name}`);
138
+ // })
139
+ // .catch((error: unknown) => {
140
+ // feedbackApi.addMessage(
141
+ // `Unable to save view: ${toError(error).message}`,
142
+ // );
143
+ // });
144
+ return;
145
+ }
146
+ case 'loadView': {
147
+ // void appApi.viewApi
148
+ // .load(appApi.viewKey, effect.name)
149
+ // .then((loadedState) => {
150
+ // if (loadedState === undefined) {
151
+ // throw new Error(`View not found: ${effect.name}`);
152
+ // }
153
+ //
154
+ // // The saved buffer may be stale because the filesystem could have
155
+ // // changed since the view was saved.
156
+ // return appApi.stateApi.resync(loadedState, appApi.loadBranches);
157
+ // })
158
+ // .then((newState) => {
159
+ // const oldState = stateRef.current;
160
+ //
161
+ // dispatch({
162
+ // kind: 'setState',
163
+ // oldState,
164
+ // newState,
165
+ // });
166
+ //
167
+ // setInputState({
168
+ // inputMode: 'normal',
169
+ // normalBuffer: '',
170
+ // });
171
+ // })
172
+ // .catch((error: unknown) => {
173
+ // onError?.(toError(error));
174
+ // });
175
+ return;
176
+ }
177
+ // TODO: Add descriptions with `...` truncation.
178
+ case 'listFolds': {
179
+ const folds = state.folds;
180
+ const foldTrees = [...appApi.foldsApi.indexes(folds)].map((index) => ({
181
+ index,
182
+ info: appApi.foldsApi.getInfoAtIndex(folds, index),
183
+ active: appApi.foldsApi.isActiveAtIndex(folds, index),
184
+ }));
185
+ const indexWidth = Math.max(...foldTrees.map(({ index }) => String(index).length));
186
+ const lines = foldTrees.map(({ index, info, active }) => {
187
+ const status = active === true ? 'active' : 'inactive';
188
+ const name = info?.name ?? '<unavailable>';
189
+ return `${String(index).padStart(indexWidth)} ${status.padEnd(8)} ${name}`;
190
+ });
191
+ setInputState({
192
+ inputMode: 'normal',
193
+ normalBuffer: '',
194
+ });
195
+ feedbackApi.addMessage(['Fold trees:', ...lines].join('\n'));
196
+ return;
197
+ }
198
+ case 'unrecognizedCommand':
199
+ feedbackApi.addMessage(`Unrecognized command: ${effect.commandLine}`, 'error');
200
+ return;
201
+ case 'quit':
202
+ setExitStatus(effect.exitMessage);
203
+ return;
204
+ }
205
+ }, [appApi, effectToAction, feedbackApi, navEntry, onError, state, stdout]);
206
+ // ===========================================================================
207
+ // Input hook
208
+ // ===========================================================================
209
+ const handleInput = useCallback((userInput) => {
210
+ const intent = userInputToIntent(userInput, inputState);
211
+ if (intent === undefined) {
212
+ return;
213
+ }
214
+ const effectResult = intentToEffect(intent, state);
215
+ if (effectResult === undefined) {
216
+ return;
217
+ }
218
+ setFeedback(undefined);
219
+ executeEffect(effectResult);
220
+ }, [executeEffect, inputState, intentToEffect, state]);
221
+ // ===========================================================================
222
+ // Render
223
+ // ===========================================================================
224
+ return (_jsx(ScreenRenderer, { view: view, feedback: feedback, feedbackApi: feedbackApi, config: config, inputState: inputState, handleInput: handleInput, onRowsChange: handleRowsChange, exitStatus: exitStatus }));
225
+ }
@@ -0,0 +1,17 @@
1
+ import type { AppApi, SerializableKey } from 'dirvi-lib';
2
+ import type { LoadingScreenRenderer } from './loading-screen-renderer.js';
3
+ import type { AppScreenRenderer } from './app-screen-renderer.js';
4
+ import { ShellApi } from '../domain/shell-api.js';
5
+ import { Initialization } from '../domain/initialization.js';
6
+ type LoadingAppProps<Id extends SerializableKey, Value, ViewKey = string> = {
7
+ appScreenRenderer: AppScreenRenderer;
8
+ loadingScreenRenderer: LoadingScreenRenderer;
9
+ initialization: Initialization;
10
+ shellApi: ShellApi;
11
+ appApi: AppApi<Id, Value, ViewKey>;
12
+ stdout?: (message: string) => void;
13
+ clipboard?: (value: string) => void;
14
+ };
15
+ export declare function AppSetup<Id extends SerializableKey, Value, ViewKey = string>({ appScreenRenderer, loadingScreenRenderer, appApi, initialization, stdout, clipboard, }: LoadingAppProps<Id, Value, ViewKey>): import("react").JSX.Element;
16
+ export {};
17
+ //# 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,KAAK,EAAE,MAAM,EAAE,eAAe,EAAS,MAAM,WAAW,CAAC;AAOhE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAGlE,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D,KAAK,eAAe,CAAC,EAAE,SAAS,eAAe,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,IAAI;IAC1E,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,qBAAqB,EAAE,qBAAqB,CAAC;IAC7C,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACnC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,wBAAgB,QAAQ,CAAC,EAAE,SAAS,eAAe,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,EAC5E,iBAAiB,EACjB,qBAAqB,EACrB,MAAM,EACN,cAAc,EACd,MAAM,EACN,SAAS,GACV,EAAE,eAAe,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,+BAgIrC"}
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { createEffectToAction, createIntentToEffect, createReducer, } from 'dirvi-lib';
4
+ import { App } from './App.js';
5
+ export function AppSetup({ appScreenRenderer, loadingScreenRenderer, appApi, initialization, stdout, clipboard, }) {
6
+ const [initialState, setInitialState] = useState();
7
+ const [config, setConfig] = useState(initialization.config);
8
+ // Load the initial configuration and keep it synchronized with changes made
9
+ // by another process or another running dirvi instance.
10
+ // useEffect(() => {
11
+ // let mounted = true;
12
+ // let latestLoad = 0;
13
+ //
14
+ // const loadConfig = (): void => {
15
+ // const loadNumber = ++latestLoad;
16
+ //
17
+ // void configApi
18
+ // .load()
19
+ // .then((nextConfig) => {
20
+ // // Ignore a stale read if a newer config read has begun meanwhile.
21
+ // if (!mounted || loadNumber !== latestLoad) {
22
+ // return;
23
+ // }
24
+ //
25
+ // setConfig(nextConfig);
26
+ // })
27
+ // .catch((cause: unknown) => {
28
+ // if (!mounted || loadNumber !== latestLoad) {
29
+ // return;
30
+ // }
31
+ //
32
+ // const nextError =
33
+ // cause instanceof Error ? cause : new Error(String(cause));
34
+ //
35
+ // setError(nextError);
36
+ // onError?.(nextError);
37
+ // });
38
+ // };
39
+ //
40
+ // // Subscribe before the first load so a change during startup is not missed.
41
+ // const unsubscribe = configApi.subscribeToResync(loadConfig);
42
+ // loadConfig();
43
+ //
44
+ // return () => {
45
+ // mounted = false;
46
+ // unsubscribe();
47
+ // };
48
+ // }, [configApi, onError]);
49
+ // Loading and setting initial state.
50
+ useEffect(() => {
51
+ let mounted = true;
52
+ appApi
53
+ .createRoot()
54
+ .then((root) => {
55
+ if (!mounted) {
56
+ return;
57
+ }
58
+ setInitialState({
59
+ root,
60
+ folds: appApi.foldsApi.create(appApi.rootId, {
61
+ name: 'default',
62
+ description: null,
63
+ }),
64
+ cursor: [],
65
+ });
66
+ })
67
+ .catch((cause) => {
68
+ const nextError = cause instanceof Error ? cause : new Error(String(cause));
69
+ if (!mounted) {
70
+ return;
71
+ }
72
+ throw nextError;
73
+ });
74
+ return () => {
75
+ mounted = false;
76
+ };
77
+ }, [appApi]);
78
+ // --- Pure function deps ---
79
+ const reducer = useMemo(() => createReducer(appApi.treeNodeApi, appApi.foldsApi), [appApi.treeNodeApi, appApi.foldsApi]);
80
+ const intentToEffect = useMemo(() => createIntentToEffect(appApi.stateApi, appApi.cursorApi, appApi.treeNodeApi), [appApi.stateApi, appApi.cursorApi, appApi.treeNodeApi]);
81
+ const effectToAction = useMemo(() => createEffectToAction(appApi.foldsApi, appApi.cursorApi, appApi.navNodeApi), [appApi.navNodeApi, appApi.cursorApi, appApi.foldsApi]);
82
+ // --- JSX ---
83
+ const LoadingScreenRenderer = loadingScreenRenderer;
84
+ if (initialState === undefined) {
85
+ return _jsx(LoadingScreenRenderer, {});
86
+ }
87
+ return (_jsx(App, { screenRenderer: appScreenRenderer, appApi: appApi, config: config, initialState: initialState, reducer: reducer, intentToEffect: intentToEffect, effectToAction: effectToAction, ...(stdout === undefined ? {} : { stdout }), ...(clipboard === undefined ? {} : { clipboard }) }));
88
+ }
@@ -0,0 +1,49 @@
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
+ /** Writes non-interactive application output. */
34
+ readonly stdout?: (message: string) => void;
35
+ /** Copies a value to the platform clipboard. */
36
+ readonly clipboard?: (value: string) => void;
37
+ };
38
+ /**
39
+ * Hosts the currently active application inside the persistent terminal shell.
40
+ *
41
+ * Shell deliberately does not depend on a concrete AppApi type. OpenApp keeps
42
+ * each application's Id, Value, and ViewKey types coherent while hiding those
43
+ * details from shell-level concerns such as startup, app selection, terminal
44
+ * integration, clipboard access, and error reporting.
45
+ *
46
+ * AppSetup receives the concrete AppApi and mounts the app-specific tree UI.
47
+ */
48
+ export declare function Shell({ loadingScreenRenderer, appScreenRenderer, initialAppOptions, initialization, packageLoader, stdout, clipboard, }: ShellAppProps): import("react").JSX.Element;
49
+ //# 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,iDAAiD;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAE5C,gDAAgD;IAChD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CAAC,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,MAAM,EACN,SAAS,GACV,EAAE,aAAa,+BAsEf"}
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { AppSetup } from './AppSetup.js';
4
+ import { createAppApiApplicator } from '../application/create-app-api.js';
5
+ /**
6
+ * Hosts the currently active application inside the persistent terminal shell.
7
+ *
8
+ * Shell deliberately does not depend on a concrete AppApi type. OpenApp keeps
9
+ * each application's Id, Value, and ViewKey types coherent while hiding those
10
+ * details from shell-level concerns such as startup, app selection, terminal
11
+ * integration, clipboard access, and error reporting.
12
+ *
13
+ * AppSetup receives the concrete AppApi and mounts the app-specific tree UI.
14
+ */
15
+ export function Shell({ loadingScreenRenderer, appScreenRenderer, initialAppOptions, initialization, packageLoader, stdout, clipboard, }) {
16
+ const initialAppReference = initialization.registeredApps.get(initialization.initialAppId);
17
+ if (initialAppReference === undefined) {
18
+ throw new Error(`Startup app "${initialization.initialAppId}" has not been registered.`);
19
+ }
20
+ const [appApiApplicator, setAppApiApplicator] = useState(undefined);
21
+ const [appError, setAppError] = useState(undefined);
22
+ const shellApi = useMemo(() => {
23
+ return {
24
+ openApp: async (appId, input) => {
25
+ const appReference = initialization.registeredApps.get(appId);
26
+ if (appReference === undefined) {
27
+ throw new Error(`App "${appId}" has not been registered.`);
28
+ }
29
+ setAppApiApplicator(undefined);
30
+ setAppError(undefined);
31
+ const appPlugin = await packageLoader.loadAppPlugin(appReference);
32
+ const appDefinitionApplicator = appPlugin.createAppDefinitionApplicator(input);
33
+ const loadedAppApiApplicator = createAppApiApplicator(appDefinitionApplicator);
34
+ setAppApiApplicator(() => loadedAppApiApplicator);
35
+ },
36
+ };
37
+ }, [initialization.registeredApps, packageLoader]);
38
+ useEffect(() => {
39
+ void shellApi
40
+ .openApp(initialization.initialAppId, initialAppOptions.input)
41
+ .catch((error) => {
42
+ setAppError(error instanceof Error ? error : new Error(String(error)));
43
+ });
44
+ }, [initialAppOptions.input, initialization.initialAppId, shellApi]);
45
+ if (appError !== undefined) {
46
+ throw appError;
47
+ }
48
+ if (appApiApplicator === undefined) {
49
+ const LoadingScreenRenderer = loadingScreenRenderer;
50
+ return _jsx(LoadingScreenRenderer, {});
51
+ }
52
+ // Result type is specified here to be AppSetup
53
+ return appApiApplicator((appApi) => (_jsx(AppSetup, { appScreenRenderer: appScreenRenderer, loadingScreenRenderer: loadingScreenRenderer, initialization: initialization, shellApi: shellApi, appApi: appApi, ...(stdout === undefined ? {} : { stdout }), ...(clipboard === undefined ? {} : { clipboard }) })));
54
+ }
@@ -0,0 +1,28 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { Feedback, FeedbackApi, InputModeState, UserInput } from 'dirvi-lib';
3
+ import type { Config } from '../domain/config.js';
4
+ import { View } from './view.js';
5
+ /**
6
+ * State and callbacks required by a React host screen.
7
+ *
8
+ * The screen owns host-specific concerns such as input subscription, viewport
9
+ * measurement, and host exit. App owns Dirvi state transitions and effects.
10
+ */
11
+ export type AppScreenProps = {
12
+ readonly view: View;
13
+ readonly feedback: Feedback | undefined;
14
+ readonly feedbackApi: FeedbackApi;
15
+ readonly config: Config;
16
+ readonly inputState: InputModeState;
17
+ readonly handleInput: (userInput: UserInput) => void;
18
+ readonly onRowsChange: (rows: number) => void;
19
+ readonly exitStatus: string | undefined;
20
+ };
21
+ /**
22
+ * A host-specific React implementation of the Dirvi application screen.
23
+ *
24
+ * InkScreen is the terminal implementation. A future web or React Native host
25
+ * can provide a different component that conforms to this same contract.
26
+ */
27
+ export type AppScreenRenderer = ComponentType<AppScreenProps>;
28
+ //# sourceMappingURL=app-screen-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-screen-renderer.d.ts","sourceRoot":"","sources":["../../src/ui/app-screen-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,cAAc,EACd,SAAS,EACV,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAC;IAEpC,QAAQ,CAAC,WAAW,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,IAAI,CAAC;IACrD,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAE9C,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,aAAa,CAAC,cAAc,CAAC,CAAC"}