dsh-life-pack 0.3.2 → 0.3.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.
@@ -0,0 +1,23 @@
1
+ /** 目录浏览器的公开门(**六家只 import 这一个名字**)。
2
+ *
3
+ * 为什么需要它:`package.json` 的 exports 一条子路径只能指一个文件,而本件分三份写
4
+ * (契约/状态/视图)。若把 `./directory-browser` 直接指到 `directory-browser-contract.js`,
5
+ * 六家就取不到「开图接线」(它在 state 那份里)。故设这一个门,把两份纯逻辑都转出;
6
+ * 视图那一份走 `./directory-browser-ui`(它要吃 React,单独一条)。
7
+ *
8
+ * 本件不含实现、也不含判断,只做转出——实现见同目录三份。
9
+ */
10
+ export { filterEntries, hasPickFn, hiddenCount, isBrowseFace, joinPath, locationLabel, parentOf, pickerModeOf, readPickAnswer, splitDraft, validateFolderName, visibleEntries, } from './directory-browser-contract.js';
11
+ export type { BrowseFailure, DirectoryBrowseFace, DirectoryCrumb, DirectoryEntry, DirectoryListing, PickEnvelope, PickerMode, PickOutcome, } from './directory-browser-contract.js';
12
+ export { canGoUp, createBaseOf, createBrowseController, createDirectoryRowBrowser, describe, entryPath, rowsOf, targetOf, } from './directory-browser-state.js';
13
+ export type { BrowseController, BrowseListener, BrowsePhase, BrowseState, DirectoryRowBrowser, } from './directory-browser-state.js';
14
+ import type { PickerMode } from './directory-browser-contract.js';
15
+ /** 一行目录配置的入口(**六家画按钮就靠这两格**):
16
+ * `mode` 决定按钮写什么(`native`=「选择文件夹…」/`browse`=「浏览…」),
17
+ * `onOpen` 是点下去的动作(系统对话框或应用内浏览器,由各家自己接线)。
18
+ * `mode === 'none'` 时调用方不该画入口——供不了就收起入口,不是失败。 */
19
+ export interface DirectoryRowEntry {
20
+ readonly mode: PickerMode;
21
+ readonly onOpen: (key: string) => Promise<void> | undefined;
22
+ }
23
+ //# sourceMappingURL=directory-browser-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-browser-api.d.ts","sourceRoot":"","sources":["../src/directory-browser-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,aAAa,EACb,SAAS,EACT,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,UAAU,EACV,kBAAkB,EAClB,cAAc,GACf,MAAM,iCAAiC,CAAC;AACzC,YAAY,EACV,aAAa,EACb,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,WAAW,GACZ,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACL,OAAO,EACP,YAAY,EACZ,sBAAsB,EACtB,yBAAyB,EACzB,QAAQ,EACR,SAAS,EACT,MAAM,EACN,QAAQ,GACT,MAAM,8BAA8B,CAAC;AACtC,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,WAAW,EACX,WAAW,EACX,mBAAmB,GACpB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE;;;kDAGkD;AAClD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC;CAC7D"}
@@ -0,0 +1,12 @@
1
+ /** 目录浏览器的公开门(**六家只 import 这一个名字**)。
2
+ *
3
+ * 为什么需要它:`package.json` 的 exports 一条子路径只能指一个文件,而本件分三份写
4
+ * (契约/状态/视图)。若把 `./directory-browser` 直接指到 `directory-browser-contract.js`,
5
+ * 六家就取不到「开图接线」(它在 state 那份里)。故设这一个门,把两份纯逻辑都转出;
6
+ * 视图那一份走 `./directory-browser-ui`(它要吃 React,单独一条)。
7
+ *
8
+ * 本件不含实现、也不含判断,只做转出——实现见同目录三份。
9
+ */
10
+ export { filterEntries, hasPickFn, hiddenCount, isBrowseFace, joinPath, locationLabel, parentOf, pickerModeOf, readPickAnswer, splitDraft, validateFolderName, visibleEntries, } from './directory-browser-contract.js';
11
+ export { canGoUp, createBaseOf, createBrowseController, createDirectoryRowBrowser, describe, entryPath, rowsOf, targetOf, } from './directory-browser-state.js';
12
+ //# sourceMappingURL=directory-browser-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-browser-api.js","sourceRoot":"","sources":["../src/directory-browser-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,aAAa,EACb,SAAS,EACT,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,UAAU,EACV,kBAAkB,EAClB,cAAc,GACf,MAAM,iCAAiC,CAAC;AAYzC,OAAO,EACL,OAAO,EACP,YAAY,EACZ,sBAAsB,EACtB,yBAAyB,EACzB,QAAQ,EACR,SAAS,EACT,MAAM,EACN,QAAQ,GACT,MAAM,8BAA8B,CAAC"}
@@ -0,0 +1,111 @@
1
+ /** 目录浏览器的纯逻辑半(**不认识宿主**:只认两格原语)。
2
+ *
3
+ * 为什么在这里:六个单品插件的设置页各有一批「值是一个目录」的行,它们要的界面是同一个东西。
4
+ * 界面的**取数**来自宿主给的两个原语(`list`/`createDirectory`),而这两个原语的形状是**调用方喂进来的**
5
+ * ——本件不 import 宿主的任何实现,也不认识 `remote.directoryPicker` 这个名字(那两件住在各家的
6
+ * `dsh-ctx.ts` 镜像里)。于是本件在 Windows 桌面、远程浏览器、SSH 三种部署下是同一份代码。
7
+ *
8
+ * 装什么:本次要浏览的那一层怎么走(进子目录/回上一级/跳面包屑)、路径怎么切出「目录部分」与
9
+ * 「筛选词」、回执怎么认。**不装**任何 IO、不装任何持久化——`list`/`createDirectory` 由调用方执行。
10
+ *
11
+ * 与平台自带浏览器件的关系:`@deepseek-ai/dsh-client-ui-directory-picker-browse` 的 `DirectoryBrowser`
12
+ * 是包内私有件(只经 `ui-workspace` 的流程空位暴露),插件侧取不到,所以这里是一份独立实现,
13
+ * 只对齐它的**交互结果**(进/选/新建),不对齐它的 DOM。
14
+ */
15
+ /** 一层目录里的一个子目录。`hidden` 由宿主判,本件只照传。 */
16
+ export interface DirectoryEntry {
17
+ readonly name: string;
18
+ readonly path: string;
19
+ readonly hidden: boolean;
20
+ }
21
+ /** 面包屑的一格:从文件系统根到当前层,每一格都是一个可跳转目标。 */
22
+ export interface DirectoryCrumb {
23
+ readonly name: string;
24
+ readonly path: string;
25
+ readonly hidden: boolean;
26
+ }
27
+ /** 一次列举的回执(形状照宿主 browse 原语:`path`/`home`/`crumbs`/`entries`/`truncated`)。 */
28
+ export interface DirectoryListing {
29
+ readonly path: string;
30
+ readonly home: string;
31
+ readonly crumbs: readonly DirectoryCrumb[];
32
+ readonly entries: readonly DirectoryEntry[];
33
+ readonly truncated: boolean;
34
+ }
35
+ /** 取数面:由调用方注入。本件只调这两个方法,别的什么都不调。 */
36
+ export interface DirectoryBrowseFace {
37
+ list(path?: string, signal?: AbortSignal): Promise<DirectoryListing>;
38
+ createDirectory(path: string, name: string): Promise<string>;
39
+ }
40
+ /** 浏览失败的人话(`code` 保留给用例断言,`message` 直接上图)。 */
41
+ export interface BrowseFailure {
42
+ readonly code: string;
43
+ readonly message: string;
44
+ }
45
+ /** 目录行入口的三态判定结果:拿到的能力是哪种,决定界面上画什么。 */
46
+ export type PickerMode = 'native' | 'browse' | 'none';
47
+ /** 平台回执的信封(各家的 `DirectoryPickerAnswer` 与此同形;本件只认这三格,不认别家的类名)。 */
48
+ export interface PickEnvelope {
49
+ readonly ok: boolean;
50
+ readonly value?: string | null;
51
+ readonly error?: {
52
+ readonly code?: string;
53
+ readonly message?: string;
54
+ };
55
+ }
56
+ /** 一次「唤起选择器」的归一结果:选中/取消/供不了,永不抛。 */
57
+ export type PickOutcome = {
58
+ readonly kind: 'picked';
59
+ readonly path: string;
60
+ } | {
61
+ readonly kind: 'cancelled';
62
+ } | {
63
+ readonly kind: 'unavailable';
64
+ readonly message: string;
65
+ };
66
+ /** 认一认某个值是不是可用的目录取数面(软依赖守卫用:认不出就当没有,绝不把页面带下来)。 */
67
+ export declare function isBrowseFace(raw: unknown): raw is DirectoryBrowseFace;
68
+ /** 认一认某个值有没有 `pick`(对应宿主 composition 里的 `native` 能力)。 */
69
+ export declare function hasPickFn(raw: unknown): boolean;
70
+ /** 三态判定:先 native(有 pick),再 browse(有两格原语),都没有=`none`。 */
71
+ export declare function pickerModeOf(raw: unknown): PickerMode;
72
+ /** 平台回执 → 三态(**永不抛**):成功回的是信封里的 `value` 不是路径;`ok:false` 是「供不了」不是「取消」。
73
+ *
74
+ * 出处:`@deepseek-ai/dsh-api-gateway/lib/client.js` 的 `invoke()`(失败不抛、成功也不回裸值),
75
+ * 第一方消费方 `@deepseek-ai/dsh-client-ui-workspace/lib/client.js:99-103` 就照这个信封拆。
76
+ * 裸串照收(老形状兜底),认不出的形状当「供不了」报出来,不当取消吞掉(#743 的教训)。 */
77
+ export declare function readPickAnswer(raw: unknown): PickOutcome;
78
+ /** 上一层:`'C:\\a\\b\\'` → `'C:\\a'`;已经是根则回自身;相对名(没有任何分隔符)也回自身。
79
+ *
80
+ * 尾分隔符先吃掉再切——否则「回上一级」会在原地打转。**盘根单独认**:`C:\` 掐掉尾分隔符会剩成
81
+ * `C:`,而 `C:` 是「当前盘」不是「上一层」,回它就会多跑一趟、还可能跨到别的盘上去。
82
+ * UNC(`\\server\share`)不特殊处理:切到头就是共享名本身,浏览停在共享根。 */
83
+ export declare function parentOf(path: string): string;
84
+ /** 把「当前路径 + 一段输入」拼成一个目标路径。 */
85
+ export declare function joinPath(directory: string, name: string): string;
86
+ /** 一层列举里,哪些行可进:隐藏目录按开关过滤,其余照宿主给的顺序。 */
87
+ export declare function visibleEntries(listing: DirectoryListing, showHidden: boolean): readonly DirectoryEntry[];
88
+ /** 这一层有多少个被藏起来的目录(用来决定「显示隐藏目录」那个开关要不要出现在图上)。 */
89
+ export declare function hiddenCount(listing: DirectoryListing): number;
90
+ /** 拿一段文本按「最后一个分隔符」切成「目录部分 + 筛选词」。
91
+ *
92
+ * 没打过任何分隔符时目录部分=null(那段文字还不指任何目录,只当筛选词用)。
93
+ * Windows 上 `/` 也当分隔符(宿主那边两种都收),POSIX 上不。 */
94
+ export declare function splitDraft(draft: string): {
95
+ readonly directory: string | null;
96
+ readonly filter: string;
97
+ };
98
+ /** 按筛选词过一层目录(大小写不敏感;空筛选词=全留)。 */
99
+ export declare function filterEntries(entries: readonly DirectoryEntry[], filter: string): readonly DirectoryEntry[];
100
+ /** 给界面用的一句话位置说明:优先显示相对 home 的说法,省得整条绝对路径占满一行。 */
101
+ export declare function locationLabel(listing: DirectoryListing): string;
102
+ /** 新建文件夹的名字合不合法(单个路径段:不许分隔符、不许空、不许 `.`/`..`)。
103
+ *
104
+ * 与宿主 browse 后端的校验同口径——这里先拦一道,是为了在图上给得出人话,不是替代宿主校验。 */
105
+ export declare function validateFolderName(name: string): {
106
+ readonly ok: true;
107
+ } | {
108
+ readonly ok: false;
109
+ readonly reason: string;
110
+ };
111
+ //# sourceMappingURL=directory-browser-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-browser-contract.d.ts","sourceRoot":"","sources":["../src/directory-browser-contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,134 @@
1
+ /** 目录浏览器的纯逻辑半(**不认识宿主**:只认两格原语)。
2
+ *
3
+ * 为什么在这里:六个单品插件的设置页各有一批「值是一个目录」的行,它们要的界面是同一个东西。
4
+ * 界面的**取数**来自宿主给的两个原语(`list`/`createDirectory`),而这两个原语的形状是**调用方喂进来的**
5
+ * ——本件不 import 宿主的任何实现,也不认识 `remote.directoryPicker` 这个名字(那两件住在各家的
6
+ * `dsh-ctx.ts` 镜像里)。于是本件在 Windows 桌面、远程浏览器、SSH 三种部署下是同一份代码。
7
+ *
8
+ * 装什么:本次要浏览的那一层怎么走(进子目录/回上一级/跳面包屑)、路径怎么切出「目录部分」与
9
+ * 「筛选词」、回执怎么认。**不装**任何 IO、不装任何持久化——`list`/`createDirectory` 由调用方执行。
10
+ *
11
+ * 与平台自带浏览器件的关系:`@deepseek-ai/dsh-client-ui-directory-picker-browse` 的 `DirectoryBrowser`
12
+ * 是包内私有件(只经 `ui-workspace` 的流程空位暴露),插件侧取不到,所以这里是一份独立实现,
13
+ * 只对齐它的**交互结果**(进/选/新建),不对齐它的 DOM。
14
+ */
15
+ /** 认一认某个值是不是可用的目录取数面(软依赖守卫用:认不出就当没有,绝不把页面带下来)。 */
16
+ export function isBrowseFace(raw) {
17
+ if (typeof raw !== 'object' || raw === null)
18
+ return false;
19
+ const face = raw;
20
+ return typeof face.list === 'function' && typeof face.createDirectory === 'function';
21
+ }
22
+ /** 认一认某个值有没有 `pick`(对应宿主 composition 里的 `native` 能力)。 */
23
+ export function hasPickFn(raw) {
24
+ if (typeof raw !== 'object' || raw === null)
25
+ return false;
26
+ return typeof raw.pick === 'function';
27
+ }
28
+ /** 三态判定:先 native(有 pick),再 browse(有两格原语),都没有=`none`。 */
29
+ export function pickerModeOf(raw) {
30
+ if (hasPickFn(raw))
31
+ return 'native';
32
+ return isBrowseFace(raw) ? 'browse' : 'none';
33
+ }
34
+ /** 平台回执 → 三态(**永不抛**):成功回的是信封里的 `value` 不是路径;`ok:false` 是「供不了」不是「取消」。
35
+ *
36
+ * 出处:`@deepseek-ai/dsh-api-gateway/lib/client.js` 的 `invoke()`(失败不抛、成功也不回裸值),
37
+ * 第一方消费方 `@deepseek-ai/dsh-client-ui-workspace/lib/client.js:99-103` 就照这个信封拆。
38
+ * 裸串照收(老形状兜底),认不出的形状当「供不了」报出来,不当取消吞掉(#743 的教训)。 */
39
+ export function readPickAnswer(raw) {
40
+ if (typeof raw === 'string')
41
+ return raw.trim() === '' ? { kind: 'cancelled' } : { kind: 'picked', path: raw };
42
+ const answer = (typeof raw === 'object' && raw !== null ? raw : {});
43
+ if (answer.ok === true) {
44
+ const value = answer.value;
45
+ return typeof value === 'string' && value.trim() !== '' ? { kind: 'picked', path: value } : { kind: 'cancelled' };
46
+ }
47
+ const detail = answer.error?.message?.trim() ?? '';
48
+ return { kind: 'unavailable', message: '打不开系统文件夹对话框' + (detail === '' ? '' : '(' + detail + ')') + ':请直接在框里填绝对路径。' };
49
+ }
50
+ /** 一段路径里最后那个分隔符的位置(Windows 上 `\` 与 `/` 都算;POSIX 上 `\` 是合法文件名字符,不算)。 */
51
+ function lastSeparator(path) {
52
+ const back = path.lastIndexOf('\\');
53
+ const slash = path.lastIndexOf('/');
54
+ // 只含 `\` 的路径按 Windows 规则解;两者都有时取更靠后的那个。
55
+ return back > slash ? back : slash;
56
+ }
57
+ /** 上一层:`'C:\\a\\b\\'` → `'C:\\a'`;已经是根则回自身;相对名(没有任何分隔符)也回自身。
58
+ *
59
+ * 尾分隔符先吃掉再切——否则「回上一级」会在原地打转。**盘根单独认**:`C:\` 掐掉尾分隔符会剩成
60
+ * `C:`,而 `C:` 是「当前盘」不是「上一层」,回它就会多跑一趟、还可能跨到别的盘上去。
61
+ * UNC(`\\server\share`)不特殊处理:切到头就是共享名本身,浏览停在共享根。 */
62
+ export function parentOf(path) {
63
+ const trimmed = path.replace(/[\\/]+$/, '');
64
+ if (trimmed === '')
65
+ return path;
66
+ const sep = path.includes('\\') ? '\\' : '/';
67
+ if (/^[A-Za-z]:$/.test(trimmed))
68
+ return trimmed + sep;
69
+ const cut = lastSeparator(trimmed);
70
+ if (cut === -1)
71
+ return trimmed;
72
+ if (cut === 0)
73
+ return path.slice(0, 1);
74
+ if (cut === 2 && /^[A-Za-z]:/.test(trimmed))
75
+ return trimmed.slice(0, 2) + sep;
76
+ return trimmed.slice(0, cut);
77
+ }
78
+ /** 把「当前路径 + 一段输入」拼成一个目标路径。 */
79
+ export function joinPath(directory, name) {
80
+ const sep = directory.includes('\\') ? '\\' : '/';
81
+ const base = directory.replace(/[\\/]+$/, '');
82
+ if (base === '')
83
+ return sep + name;
84
+ return base + sep + name;
85
+ }
86
+ /** 一层列举里,哪些行可进:隐藏目录按开关过滤,其余照宿主给的顺序。 */
87
+ export function visibleEntries(listing, showHidden) {
88
+ return showHidden ? listing.entries : listing.entries.filter((entry) => !entry.hidden);
89
+ }
90
+ /** 这一层有多少个被藏起来的目录(用来决定「显示隐藏目录」那个开关要不要出现在图上)。 */
91
+ export function hiddenCount(listing) {
92
+ return listing.entries.filter((entry) => entry.hidden).length;
93
+ }
94
+ /** 拿一段文本按「最后一个分隔符」切成「目录部分 + 筛选词」。
95
+ *
96
+ * 没打过任何分隔符时目录部分=null(那段文字还不指任何目录,只当筛选词用)。
97
+ * Windows 上 `/` 也当分隔符(宿主那边两种都收),POSIX 上不。 */
98
+ export function splitDraft(draft) {
99
+ const cut = lastSeparator(draft);
100
+ if (cut === -1)
101
+ return { directory: null, filter: draft };
102
+ return { directory: draft.slice(0, cut + 1), filter: draft.slice(cut + 1) };
103
+ }
104
+ /** 按筛选词过一层目录(大小写不敏感;空筛选词=全留)。 */
105
+ export function filterEntries(entries, filter) {
106
+ const needle = filter.trim().toLowerCase();
107
+ if (needle === '')
108
+ return entries;
109
+ return entries.filter((entry) => entry.name.toLowerCase().includes(needle));
110
+ }
111
+ /** 给界面用的一句话位置说明:优先显示相对 home 的说法,省得整条绝对路径占满一行。 */
112
+ export function locationLabel(listing) {
113
+ const home = listing.home.replace(/[\\/]+$/, '');
114
+ if (home !== '' && listing.path !== home && listing.path.startsWith(home)) {
115
+ const tail = listing.path.slice(home.length).replace(/^[\\/]+/, '');
116
+ if (tail !== '')
117
+ return '~' + (listing.path.includes('\\') ? '\\' : '/') + tail;
118
+ }
119
+ return listing.path;
120
+ }
121
+ /** 新建文件夹的名字合不合法(单个路径段:不许分隔符、不许空、不许 `.`/`..`)。
122
+ *
123
+ * 与宿主 browse 后端的校验同口径——这里先拦一道,是为了在图上给得出人话,不是替代宿主校验。 */
124
+ export function validateFolderName(name) {
125
+ const trimmed = name.trim();
126
+ if (trimmed === '')
127
+ return { ok: false, reason: '文件夹名不能为空。' };
128
+ if (/[\\/]/.test(trimmed))
129
+ return { ok: false, reason: '文件夹名里不能带路径分隔符。' };
130
+ if (trimmed === '.' || trimmed === '..')
131
+ return { ok: false, reason: '「.」与「..」不是文件夹名。' };
132
+ return { ok: true };
133
+ }
134
+ //# sourceMappingURL=directory-browser-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-browser-contract.js","sourceRoot":"","sources":["../src/directory-browser-contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ /** 文件浏览器的操作半:把「进哪一层、这一层显示什么、选到了什么」算成状态,**IO 全部由调用方注入**。
2
+ *
3
+ * 分两层的原因:本件(操作)在 Node 里就能直测——喂一个假的取数面,就能把「进目录/回上一级/
4
+ * 跳面包屑/筛选/新建文件夹/失败出人话」全跑一遍;视图那半(`directory-browser-ui.ts`)
5
+ * 只负责把这份状态画成 DOM。两边都不认识宿主。
6
+ *
7
+ * 数据来源只有两格:`list(path?)` 与 `createDirectory(path, name)`(调用方注入的 `DirectoryBrowseFace`)。
8
+ */
9
+ import type { BrowseFailure, DirectoryBrowseFace, DirectoryEntry, DirectoryListing } from './directory-browser-contract.js';
10
+ /** 一层列举的三态:加载中/拿到了/这一层读不出来。 */
11
+ export type BrowsePhase = 'idle' | 'loading' | 'ready' | 'failed';
12
+ /** 图上的全部状态。视图只读它,不改它。 */
13
+ export interface BrowseState {
14
+ readonly phase: BrowsePhase;
15
+ readonly listing: DirectoryListing | null;
16
+ /** 这一层读失败时的人话;`phase==='failed'` 时一定有。 */
17
+ readonly failure: BrowseFailure | null;
18
+ /** 当前选中的子目录(点行选中;选中后再点「打开」就是它)。 */
19
+ readonly selected: string | null;
20
+ /** 面包屑上那一层的编号,只用于画图。 */
21
+ readonly showHidden: boolean;
22
+ /** 路径框里的草稿(用户可能正在编辑)。 */
23
+ readonly draft: string;
24
+ /** 草稿指向的那一层已经列举过之后,用来按末段过滤的词。 */
25
+ readonly filter: string;
26
+ /** 正在建的文件夹名;null=没在建。 */
27
+ readonly creating: string | null;
28
+ /** 上一次操作的失败人话(新建失败等);看一眼就好,下一次操作清掉。 */
29
+ readonly notice: string | null;
30
+ }
31
+ /** 订阅者拿到的状态(不可变,每次变更是新对象)。 */
32
+ export type BrowseListener = (state: BrowseState) => void;
33
+ /** 操作面:视图与用例都只用这些方法。 */
34
+ export interface BrowseController {
35
+ getState(): BrowseState;
36
+ subscribe(listener: BrowseListener): () => void;
37
+ /** 开图:初次列举(`list()` 不带路径=宿主给的家目录)。 */
38
+ open(): Promise<void>;
39
+ /** 进某一层(绝对路径,或 `list()` 的默认家目录当入参为 null)。 */
40
+ enter(path: string | null): Promise<void>;
41
+ /** 回上一级(当前层就是根时原地不动)。 */
42
+ up(): Promise<void>;
43
+ /** 采纳当前选择(选中行,没有选中就用当前层)并回调 `onPicked`。 */
44
+ pick(): void;
45
+ /** 取消:关图,什么都不回调。 */
46
+ cancel(): void;
47
+ /** 选中一个子目录(再点一次同一条=取消选中)。 */
48
+ select(path: string): void;
49
+ /** 切换隐藏目录。 */
50
+ toggleHidden(): void;
51
+ /** 改路径框草稿(不列举,只更新草稿与筛选词)。 */
52
+ setDraft(draft: string): void;
53
+ /** 提交路径框草稿:指到哪一层就去哪一层。 */
54
+ commitDraft(): Promise<void>;
55
+ /** 开「新建文件夹」;传 null 收起。 */
56
+ setCreating(name: string | null): void;
57
+ /** 在当前层(或选中的那一层)下建一个新文件夹,成功后进它并选中它。 */
58
+ createFolder(name: string): Promise<void>;
59
+ /** 关图(与 `cancel` 同义,供宿主侧统一收口)。 */
60
+ close(): void;
61
+ }
62
+ /** 建一个浏览器控制器。取数、回调、初值全部由调用方给。 */
63
+ export declare function createBrowseController(deps: {
64
+ readonly face: DirectoryBrowseFace;
65
+ readonly initialPath: string;
66
+ readonly onPicked: (path: string) => void;
67
+ readonly onClose: () => void;
68
+ /** 失败人话的前缀,默认「浏览失败」。 */
69
+ readonly failureLabel?: string;
70
+ }): BrowseController;
71
+ /** 图上这一层要画的行(按隐藏开关与草稿末段过滤之后的)。 */
72
+ export declare function rowsOf(state: BrowseState): readonly DirectoryEntry[];
73
+ /** 图上这一层能不能回上一级(根目录不能)。 */
74
+ export declare function canGoUp(state: BrowseState): boolean;
75
+ /** 「打开」这颗按钮的目标:选中行优先,没选中就是当前层。 */
76
+ export declare function targetOf(state: BrowseState): string | null;
77
+ /** 新建文件夹那一行要展示的位置("在 <这里> 下新建")。 */
78
+ export declare function createBaseOf(state: BrowseState): string | null;
79
+ /** 一段路径拼成可读的一句话(给用例与调试用;图上不直接显示)。 */
80
+ export declare function describe(state: BrowseState): string;
81
+ /** 一行子目录的完整路径(图上点行时用)。 */
82
+ export declare function entryPath(directory: string, entry: DirectoryEntry): string;
83
+ /** 目录行接线的返回:**不碰 React**——`actions` 原样喂给 `directory-browser-ui` 的组件 props,
84
+ * `isOpen`/`setOpen` 由调用方自己接进它的状态(宿主怎么重画是它的事,本件不管)。 */
85
+ export interface DirectoryRowBrowser {
86
+ readonly actions: {
87
+ readonly onPick: () => void;
88
+ readonly onClose: () => void;
89
+ readonly onEnter: (path: string) => void;
90
+ readonly onUp: () => void;
91
+ readonly onSelect: (path: string) => void;
92
+ readonly onToggleHidden: () => void;
93
+ readonly onDraft: (draft: string) => void;
94
+ readonly onCommitDraft: () => void;
95
+ readonly onCreate: (name: string) => void;
96
+ readonly onCreatingChange: (name: string | null) => void;
97
+ };
98
+ /** 开图(初次列举家目录),并把「开着」交给调用方自己记。 */
99
+ open(): Promise<void>;
100
+ state(): BrowseState;
101
+ /** 一段可读的当前状态(用例与调试用)。 */
102
+ summary(): string;
103
+ }
104
+ /** 建一份「目录行接线」:给一个取数面与初值,回一组动作。
105
+ *
106
+ * 只做接线:把控制器算出来的状态与回调打包好,不取数、不画图、不认宿主。 */
107
+ export declare function createDirectoryRowBrowser(deps: {
108
+ readonly face: DirectoryBrowseFace;
109
+ readonly initialPath: string;
110
+ readonly onPicked: (path: string) => void;
111
+ readonly onClosed?: () => void;
112
+ readonly failureLabel?: string;
113
+ }): DirectoryRowBrowser;
114
+ //# sourceMappingURL=directory-browser-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-browser-state.d.ts","sourceRoot":"","sources":["../src/directory-browser-state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,203 @@
1
+ /** 文件浏览器的操作半:把「进哪一层、这一层显示什么、选到了什么」算成状态,**IO 全部由调用方注入**。
2
+ *
3
+ * 分两层的原因:本件(操作)在 Node 里就能直测——喂一个假的取数面,就能把「进目录/回上一级/
4
+ * 跳面包屑/筛选/新建文件夹/失败出人话」全跑一遍;视图那半(`directory-browser-ui.ts`)
5
+ * 只负责把这份状态画成 DOM。两边都不认识宿主。
6
+ *
7
+ * 数据来源只有两格:`list(path?)` 与 `createDirectory(path, name)`(调用方注入的 `DirectoryBrowseFace`)。
8
+ */
9
+ import { filterEntries, joinPath, parentOf, splitDraft, validateFolderName, visibleEntries, } from './directory-browser-contract.js';
10
+ /** 建一个浏览器控制器。取数、回调、初值全部由调用方给。 */
11
+ export function createBrowseController(deps) {
12
+ const label = deps.failureLabel ?? '浏览失败';
13
+ const listeners = new Set();
14
+ let state = {
15
+ phase: 'idle',
16
+ listing: null,
17
+ failure: null,
18
+ selected: null,
19
+ showHidden: false,
20
+ draft: deps.initialPath,
21
+ filter: '',
22
+ creating: null,
23
+ notice: null,
24
+ };
25
+ /** 每次列举带一个序号:晚回来的旧回执直接丢掉,不许覆盖新一层。 */
26
+ let generation = 0;
27
+ const emit = (next) => {
28
+ state = { ...state, ...next };
29
+ for (const listener of [...listeners])
30
+ listener(state);
31
+ };
32
+ const humanize = (cause) => {
33
+ const raw = (typeof cause === 'object' && cause !== null ? cause : {});
34
+ const message = typeof raw.message === 'string' && raw.message.trim() !== '' ? raw.message.trim() : String(cause);
35
+ const code = typeof raw.code === 'string' && raw.code !== '' ? raw.code : 'browse-failed';
36
+ return { code, message: `${label}:${message}` };
37
+ };
38
+ const load = async (path) => {
39
+ const mine = ++generation;
40
+ emit({ phase: 'loading', failure: null, notice: null });
41
+ try {
42
+ const listing = path === null ? await deps.face.list() : await deps.face.list(path);
43
+ if (mine !== generation)
44
+ return;
45
+ emit({
46
+ phase: 'ready',
47
+ listing,
48
+ failure: null,
49
+ selected: null,
50
+ draft: listing.path,
51
+ filter: '',
52
+ });
53
+ }
54
+ catch (cause) {
55
+ if (mine !== generation)
56
+ return;
57
+ emit({ phase: 'failed', listing: null, failure: humanize(cause), selected: null });
58
+ }
59
+ };
60
+ /** 草稿指到的那一层已经列举过时,按末段筛当前这一层的行;否则不过滤。
61
+ *
62
+ * 两边都先掐掉尾分隔符再比:草稿那边切开后天然带尾分隔符(`C:\a\` + `b`),
63
+ * 而这一层的 `path` 由宿主给、不带尾分隔符。 */
64
+ const applyDraft = (draft) => {
65
+ const { directory, filter } = splitDraft(draft);
66
+ const listing = state.listing;
67
+ const sameLevel = listing !== null && directory !== null && normalize(directory) === normalize(listing.path);
68
+ emit({ draft, filter: sameLevel ? filter : '' });
69
+ };
70
+ const normalize = (path) => path.replace(/[\\/]+$/, '');
71
+ return {
72
+ getState: () => state,
73
+ subscribe(listener) {
74
+ listeners.add(listener);
75
+ return () => listeners.delete(listener);
76
+ },
77
+ open: () => load(null),
78
+ enter: (path) => load(path),
79
+ async up() {
80
+ const listing = state.listing;
81
+ if (listing === null)
82
+ return;
83
+ const parent = parentOf(listing.path);
84
+ if (parent === listing.path)
85
+ return;
86
+ await load(parent);
87
+ },
88
+ pick() {
89
+ const target = state.selected ?? state.listing?.path ?? null;
90
+ if (target === null)
91
+ return;
92
+ deps.onPicked(target);
93
+ },
94
+ cancel() {
95
+ deps.onClose();
96
+ },
97
+ close() {
98
+ deps.onClose();
99
+ },
100
+ select(path) {
101
+ emit({ selected: state.selected === path ? null : path, notice: null });
102
+ },
103
+ toggleHidden() {
104
+ emit({ showHidden: !state.showHidden });
105
+ },
106
+ setDraft(draft) {
107
+ applyDraft(draft);
108
+ },
109
+ async commitDraft() {
110
+ const trimmed = state.draft.trim();
111
+ if (trimmed === '')
112
+ return;
113
+ await load(trimmed);
114
+ },
115
+ setCreating(name) {
116
+ emit({ creating: name, notice: null });
117
+ },
118
+ async createFolder(name) {
119
+ const checked = validateFolderName(name);
120
+ if (!checked.ok) {
121
+ emit({ notice: checked.reason });
122
+ return;
123
+ }
124
+ const base = state.selected ?? state.listing?.path ?? null;
125
+ if (base === null) {
126
+ emit({ notice: '还没有打开任何目录,无法新建文件夹。' });
127
+ return;
128
+ }
129
+ try {
130
+ const created = await deps.face.createDirectory(base, name.trim());
131
+ emit({ creating: null, notice: null });
132
+ await load(created);
133
+ emit({ selected: created });
134
+ }
135
+ catch (cause) {
136
+ const failure = humanize(cause);
137
+ emit({ notice: failure.message, creating: null });
138
+ }
139
+ },
140
+ };
141
+ }
142
+ /** 图上这一层要画的行(按隐藏开关与草稿末段过滤之后的)。 */
143
+ export function rowsOf(state) {
144
+ if (state.listing === null)
145
+ return [];
146
+ return filterEntries(visibleEntries(state.listing, state.showHidden), state.filter);
147
+ }
148
+ /** 图上这一层能不能回上一级(根目录不能)。 */
149
+ export function canGoUp(state) {
150
+ if (state.listing === null)
151
+ return false;
152
+ return parentOf(state.listing.path) !== state.listing.path;
153
+ }
154
+ /** 「打开」这颗按钮的目标:选中行优先,没选中就是当前层。 */
155
+ export function targetOf(state) {
156
+ return state.selected ?? state.listing?.path ?? null;
157
+ }
158
+ /** 新建文件夹那一行要展示的位置("在 <这里> 下新建")。 */
159
+ export function createBaseOf(state) {
160
+ return state.selected ?? state.listing?.path ?? null;
161
+ }
162
+ /** 一段路径拼成可读的一句话(给用例与调试用;图上不直接显示)。 */
163
+ export function describe(state) {
164
+ if (state.phase === 'failed')
165
+ return `${state.failure?.code ?? 'browse-failed'} @ ${state.draft}`;
166
+ if (state.listing === null)
167
+ return `${state.phase}`;
168
+ return `${state.listing.path}(${rowsOf(state).length} 行)`;
169
+ }
170
+ /** 一行子目录的完整路径(图上点行时用)。 */
171
+ export function entryPath(directory, entry) {
172
+ return entry.path === '' ? joinPath(directory, entry.name) : entry.path;
173
+ }
174
+ /** 建一份「目录行接线」:给一个取数面与初值,回一组动作。
175
+ *
176
+ * 只做接线:把控制器算出来的状态与回调打包好,不取数、不画图、不认宿主。 */
177
+ export function createDirectoryRowBrowser(deps) {
178
+ const controller = createBrowseController({
179
+ face: deps.face,
180
+ initialPath: deps.initialPath,
181
+ onPicked: deps.onPicked,
182
+ onClose: () => deps.onClosed?.(),
183
+ ...(deps.failureLabel === undefined ? {} : { failureLabel: deps.failureLabel }),
184
+ });
185
+ return {
186
+ open: () => controller.open(),
187
+ state: () => controller.getState(),
188
+ summary: () => describe(controller.getState()),
189
+ actions: {
190
+ onPick: () => controller.pick(),
191
+ onClose: () => controller.cancel(),
192
+ onEnter: (path) => void controller.enter(path),
193
+ onUp: () => void controller.up(),
194
+ onSelect: (path) => controller.select(path),
195
+ onToggleHidden: () => controller.toggleHidden(),
196
+ onDraft: (draft) => controller.setDraft(draft),
197
+ onCommitDraft: () => void controller.commitDraft(),
198
+ onCreate: (name) => void controller.createFolder(name),
199
+ onCreatingChange: (name) => controller.setCreating(name),
200
+ },
201
+ };
202
+ }
203
+ //# sourceMappingURL=directory-browser-state.js.map