dsh-wsl-workspace 0.2.2 → 0.2.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.
- package/README.de.md +49 -0
- package/README.es.md +49 -0
- package/README.fr.md +49 -0
- package/README.ja.md +49 -0
- package/README.ko.md +49 -0
- package/README.md +1 -1
- package/README.pt.md +49 -0
- package/README.ru.md +49 -0
- package/README.zh.md +3 -3
- package/lib/client.js +76 -5
- package/lib/client.js.map +1 -1
- package/lib/fs.js +1 -1
- package/lib/index.js +92 -13
- package/lib/index.js.map +1 -1
- package/lib/{paths-DBaSmi7x.js → paths-BDE1NVOv.js} +17 -2
- package/lib/paths-BDE1NVOv.js.map +1 -0
- package/lib/shell.js +2 -2
- package/lib/{wsl-GjkUifnx.js → wsl-CW3VPEIA.js} +41 -3
- package/lib/wsl-CW3VPEIA.js.map +1 -0
- package/package.json +8 -3
- package/src/client/AddWslWorkspace.tsx +349 -346
- package/src/client/api.ts +124 -104
- package/src/client/index.ts +233 -194
- package/src/index.ts +108 -13
- package/src/shared/paths.ts +175 -159
- package/src/shared/wsl-credentials.ts +135 -85
- package/image-1.png +0 -0
- package/image.png +0 -0
- package/lib/paths-DBaSmi7x.js.map +0 -1
- package/lib/wsl-GjkUifnx.js.map +0 -1
|
@@ -1,346 +1,349 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar footer action that opens the "Add WSL workspace" dialog. In the
|
|
3
|
-
* wide sidebar it renders a labeled row; in the 56px rail it collapses to a
|
|
4
|
-
* 36px icon button (both honor the shell's `{ wide }` owner share).
|
|
5
|
-
*
|
|
6
|
-
* Registration omits the typed `locale:` seat (the `wslWorkspace` namespace is
|
|
7
|
-
* not merged into `LocaleNamespaceMap`), so the injected face carries the
|
|
8
|
-
* bound translate function instead.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type * as React from 'react'
|
|
12
|
-
import { useEffect, useRef, useState, type UIEventHandler } from 'react'
|
|
13
|
-
import { isAbsoluteLinuxPath, isValidWslUsername,
|
|
14
|
-
import type { WslDirListing, WslPathCheck } from './api.ts'
|
|
15
|
-
|
|
16
|
-
/** Result-level shape of a browse/check/list call we surface uniformly. */
|
|
17
|
-
interface ApiCall<T> {
|
|
18
|
-
value: T
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** The inject face: plain data + callbacks the dialog drives. */
|
|
22
|
-
export interface AddWslWorkspaceInjected {
|
|
23
|
-
/**
|
|
24
|
-
* Confirm the deployment exposes a healthy `wsl` preset.
|
|
25
|
-
* @returns undefined when healthy, else a Chinese/English message to show.
|
|
26
|
-
*/
|
|
27
|
-
checkPreset(): Promise<string | undefined>
|
|
28
|
-
/** List the WSL distros installed on the host. */
|
|
29
|
-
listDistros(): Promise<string[]>
|
|
30
|
-
/** List one Linux directory level inside a distro. */
|
|
31
|
-
listDir(distro: string, path: string): Promise<WslDirListing>
|
|
32
|
-
/** Check a Linux path's existence/directory facts. */
|
|
33
|
-
check(distro: string, path: string): Promise<WslPathCheck>
|
|
34
|
-
/**
|
|
35
|
-
* Register a
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* @
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const [
|
|
82
|
-
const [
|
|
83
|
-
const [
|
|
84
|
-
const [
|
|
85
|
-
const [
|
|
86
|
-
const [
|
|
87
|
-
const [
|
|
88
|
-
const [
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (seq === browseSeq.current)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
</
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
{
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
{
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
</button>
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar footer action that opens the "Add WSL workspace" dialog. In the
|
|
3
|
+
* wide sidebar it renders a labeled row; in the 56px rail it collapses to a
|
|
4
|
+
* 36px icon button (both honor the shell's `{ wide }` owner share).
|
|
5
|
+
*
|
|
6
|
+
* Registration omits the typed `locale:` seat (the `wslWorkspace` namespace is
|
|
7
|
+
* not merged into `LocaleNamespaceMap`), so the injected face carries the
|
|
8
|
+
* bound translate function instead.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type * as React from 'react'
|
|
12
|
+
import { useEffect, useRef, useState, type UIEventHandler } from 'react'
|
|
13
|
+
import { isAbsoluteLinuxPath, isValidWslUsername, normalizeLinuxPath } from '../shared/paths.ts'
|
|
14
|
+
import type { WslDirListing, WslPathCheck } from './api.ts'
|
|
15
|
+
|
|
16
|
+
/** Result-level shape of a browse/check/list call we surface uniformly. */
|
|
17
|
+
interface ApiCall<T> {
|
|
18
|
+
value: T
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The inject face: plain data + callbacks the dialog drives. */
|
|
22
|
+
export interface AddWslWorkspaceInjected {
|
|
23
|
+
/**
|
|
24
|
+
* Confirm the deployment exposes a healthy `wsl` preset.
|
|
25
|
+
* @returns undefined when healthy, else a Chinese/English message to show.
|
|
26
|
+
*/
|
|
27
|
+
checkPreset(): Promise<string | undefined>
|
|
28
|
+
/** List the WSL distros installed on the host. */
|
|
29
|
+
listDistros(): Promise<string[]>
|
|
30
|
+
/** List one Linux directory level inside a distro. */
|
|
31
|
+
listDir(distro: string, path: string): Promise<WslDirListing>
|
|
32
|
+
/** Check a Linux path's existence/directory facts. */
|
|
33
|
+
check(distro: string, path: string): Promise<WslPathCheck>
|
|
34
|
+
/**
|
|
35
|
+
* Register a WSL workspace and start a session in it. `/mnt/<drive>`
|
|
36
|
+
* paths register under their Windows drive spelling (9P cannot serve
|
|
37
|
+
* drvfs); ext4 paths register as `\\wsl.localhost\<distro>\...` UNC.
|
|
38
|
+
* @param linuxPath - the absolute Linux workspace path.
|
|
39
|
+
* @param username - optional Linux user for the session (empty = distro default).
|
|
40
|
+
* @param distro - the WSL distribution the path belongs to.
|
|
41
|
+
* @returns undefined on success, else a message to show.
|
|
42
|
+
*/
|
|
43
|
+
createWorkspace(linuxPath: string, username: string, distro: string): Promise<string | undefined>
|
|
44
|
+
/** Translate a `wslWorkspace` dictionary key (follows the DeepSeek Harness locale). */
|
|
45
|
+
t: (key: string, params?: Record<string, unknown>) => string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Full component props: the owner share plus the injected face. */
|
|
49
|
+
export interface AddWslWorkspaceProps extends AddWslWorkspaceInjected {
|
|
50
|
+
/** Whether the sidebar renders wide content (false = 56px rail). */
|
|
51
|
+
wide: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the Linux child path one level below a parent, for the breadcrumb/
|
|
56
|
+
* browse drill.
|
|
57
|
+
* @param parent - the currently listed absolute path (`/` for root).
|
|
58
|
+
* @param name - the child directory name.
|
|
59
|
+
* @returns the child's absolute Linux path.
|
|
60
|
+
*/
|
|
61
|
+
export function dirChildPath(parent: string, name: string): string {
|
|
62
|
+
return parent === '/' ? `/${name}` : `${parent}/${name}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A tiny inline terminal glyph for the dialog's directory rows. */
|
|
66
|
+
function WslGlyph({ size = 16 }: { size?: number }): React.ReactElement {
|
|
67
|
+
return (
|
|
68
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
69
|
+
<rect x="2.5" y="4.5" width="19" height="15" rx="2.5" stroke="currentColor" strokeWidth="1.6" />
|
|
70
|
+
<path d="M6 9l3.2 2.6L6 14" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
|
|
71
|
+
<path d="M12 14h5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
|
72
|
+
</svg>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The "Add WSL workspace…" footer action and its dialog.
|
|
78
|
+
* @param props - owner share + injected face.
|
|
79
|
+
*/
|
|
80
|
+
export function AddWslWorkspace({ wide, t, checkPreset, listDistros, listDir, check, createWorkspace }: AddWslWorkspaceProps): React.ReactElement | null {
|
|
81
|
+
const [open, setOpen] = useState(false)
|
|
82
|
+
const [opening, setOpening] = useState(false)
|
|
83
|
+
const [distros, setDistros] = useState<string[]>([])
|
|
84
|
+
const [distro, setDistro] = useState('')
|
|
85
|
+
const [pathInput, setPathInput] = useState('/home/')
|
|
86
|
+
const [username, setUsername] = useState('')
|
|
87
|
+
const [listing, setListing] = useState<WslDirListing | null>(null)
|
|
88
|
+
const [browsePath, setBrowsePath] = useState('/')
|
|
89
|
+
const [browsing, setBrowsing] = useState(false)
|
|
90
|
+
const [error, setError] = useState<string | null>(null)
|
|
91
|
+
const [busy, setBusy] = useState(false)
|
|
92
|
+
// Monotone browse-request sequence: stale responses for a superseded browse are dropped.
|
|
93
|
+
const browseSeq = useRef(0)
|
|
94
|
+
|
|
95
|
+
const refreshBrowse = async (root: string, targetDistro: string): Promise<void> => {
|
|
96
|
+
const seq = ++browseSeq.current
|
|
97
|
+
setBrowsing(true)
|
|
98
|
+
setBrowsePath(root)
|
|
99
|
+
try {
|
|
100
|
+
const value = await listDir(targetDistro, root)
|
|
101
|
+
if (seq === browseSeq.current) setListing(value)
|
|
102
|
+
} catch {
|
|
103
|
+
// A failed browse (permission, missing dir) is non-fatal: keep old listing.
|
|
104
|
+
if (seq === browseSeq.current) {
|
|
105
|
+
setListing(null)
|
|
106
|
+
setError((previous) => previous ?? t('error.loadDir'))
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
if (seq === browseSeq.current) setBrowsing(false)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!open) return
|
|
115
|
+
let cancelled = false
|
|
116
|
+
setError(null)
|
|
117
|
+
setOpening(true)
|
|
118
|
+
void (async () => {
|
|
119
|
+
let presetIssue: string | undefined
|
|
120
|
+
try {
|
|
121
|
+
presetIssue = await checkPreset()
|
|
122
|
+
} catch {
|
|
123
|
+
presetIssue = t('error.loadDistros')
|
|
124
|
+
}
|
|
125
|
+
let names: string[]
|
|
126
|
+
try {
|
|
127
|
+
names = await listDistros()
|
|
128
|
+
} catch {
|
|
129
|
+
if (cancelled) return
|
|
130
|
+
setOpening(false)
|
|
131
|
+
setError(t('error.loadDistros'))
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (cancelled) return
|
|
135
|
+
setDistros(names)
|
|
136
|
+
const first = names[0] ?? ''
|
|
137
|
+
setDistro(first)
|
|
138
|
+
// The default browse root walks from `/`; the input defaults to `/home/`.
|
|
139
|
+
setBrowsing(true)
|
|
140
|
+
setOpening(false)
|
|
141
|
+
if (presetIssue !== undefined) setError(presetIssue)
|
|
142
|
+
if (first !== '') void refreshBrowse('/', first)
|
|
143
|
+
})()
|
|
144
|
+
return () => { cancelled = true }
|
|
145
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- run once per open against current t.
|
|
146
|
+
}, [open])
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!open) return
|
|
150
|
+
const onKey = (event: KeyboardEvent): void => {
|
|
151
|
+
if (event.key === 'Escape' && !busy) setOpen(false)
|
|
152
|
+
}
|
|
153
|
+
window.addEventListener('keydown', onKey)
|
|
154
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
155
|
+
}, [open, busy])
|
|
156
|
+
|
|
157
|
+
if (!open) {
|
|
158
|
+
// A W-letter action beside Settings at the sidebar foot; the title
|
|
159
|
+
// carries the label in both wide and rail states.
|
|
160
|
+
return (
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
className={wide ? 'dww-action dww-action--wide' : 'dww-action dww-action--rail'}
|
|
164
|
+
title={t('action.title')}
|
|
165
|
+
aria-label={t('action.title')}
|
|
166
|
+
onClick={() => setOpen(true)}
|
|
167
|
+
>
|
|
168
|
+
<span className="dww-letter" aria-hidden="true">W</span>
|
|
169
|
+
</button>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const onDrill = (name: string): void => {
|
|
174
|
+
const next = dirChildPath(listing?.path ?? browsePath, name)
|
|
175
|
+
setPathInput(next)
|
|
176
|
+
void refreshBrowse(next, distro)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const onUp = (): void => {
|
|
180
|
+
const parent = listing?.parent ?? null
|
|
181
|
+
if (parent === null) return
|
|
182
|
+
setPathInput(parent)
|
|
183
|
+
void refreshBrowse(parent, distro)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const onDistroChange = (value: string): void => {
|
|
187
|
+
setDistro(value)
|
|
188
|
+
void refreshBrowse(browsePath, value)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const onCheck = async (): Promise<void> => {
|
|
192
|
+
const path = normalizeLinuxPath(pathInput)
|
|
193
|
+
setError(null)
|
|
194
|
+
if (!isAbsoluteLinuxPath(path) || path === '/') {
|
|
195
|
+
setError(t('error.invalidPath'))
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
let facts: WslPathCheck
|
|
199
|
+
try {
|
|
200
|
+
facts = await check(distro, path)
|
|
201
|
+
} catch {
|
|
202
|
+
setError(t('error.pathNotFound'))
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
if (!facts.exists || !facts.isDirectory) {
|
|
206
|
+
setError(t('error.pathNotFound'))
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
void refreshBrowse(path, distro)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const onConfirm = async (): Promise<void> => {
|
|
213
|
+
const path = normalizeLinuxPath(pathInput)
|
|
214
|
+
setError(null)
|
|
215
|
+
if (!isAbsoluteLinuxPath(path) || path === '/') {
|
|
216
|
+
// A workspace at the distribution root would make every session start
|
|
217
|
+
// at `/`; the check flow rejects it and confirm must agree.
|
|
218
|
+
setError(t('error.invalidPath'))
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
const user = username.trim()
|
|
222
|
+
if (user !== '' && !isValidWslUsername(user)) {
|
|
223
|
+
setError(t('error.invalidUsername'))
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
setBusy(true)
|
|
227
|
+
try {
|
|
228
|
+
let facts: WslPathCheck
|
|
229
|
+
try {
|
|
230
|
+
facts = await check(distro, path)
|
|
231
|
+
} catch {
|
|
232
|
+
setError(t('error.pathNotFound'))
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
if (!facts.exists || !facts.isDirectory) {
|
|
236
|
+
setError(t('error.pathNotFound'))
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
const failure = await createWorkspace(path, user, distro)
|
|
240
|
+
if (failure !== undefined) {
|
|
241
|
+
setError(failure)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
setOpen(false)
|
|
245
|
+
} finally {
|
|
246
|
+
setBusy(false)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const children = (listing?.entries.filter(entry => entry.kind === 'directory') ?? []).map(entry => entry.name)
|
|
251
|
+
const maskClick = (): void => { if (!busy) setOpen(false) }
|
|
252
|
+
const listScroll: UIEventHandler<HTMLDivElement> = () => { /* scroll container handles overflow */ }
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div className="dww-overlay">
|
|
256
|
+
<div className="dww-overlay-mask" onClick={maskClick} />
|
|
257
|
+
<div className="dww-card" role="dialog" aria-modal="true" aria-label={t('dialog.title')}>
|
|
258
|
+
<div className="dww-header">
|
|
259
|
+
<h2 className="dww-title">{t('dialog.title')}</h2>
|
|
260
|
+
<button type="button" className="dww-close" aria-label={t('dialog.cancel')} onClick={maskClick}>
|
|
261
|
+
✕
|
|
262
|
+
</button>
|
|
263
|
+
</div>
|
|
264
|
+
<div className="dww-body">
|
|
265
|
+
{error !== null ? (
|
|
266
|
+
<div className="dww-error">
|
|
267
|
+
{error}
|
|
268
|
+
<button type="button" className="dww-retry" onClick={() => setError(null)}>{t('dialog.retry')}</button>
|
|
269
|
+
</div>
|
|
270
|
+
) : null}
|
|
271
|
+
<div className="dww-field">
|
|
272
|
+
<label className="dww-field-label" htmlFor="dww-distro">{t('dialog.distro')}</label>
|
|
273
|
+
<select
|
|
274
|
+
id="dww-distro"
|
|
275
|
+
className="dww-select"
|
|
276
|
+
value={distro}
|
|
277
|
+
disabled={opening || busy}
|
|
278
|
+
onChange={event => onDistroChange(event.target.value)}
|
|
279
|
+
>
|
|
280
|
+
{distros.length === 0
|
|
281
|
+
? <option value="">{opening ? t('dialog.loading') : ''}</option>
|
|
282
|
+
: distros.map(name => <option key={name} value={name}>{name}</option>)}
|
|
283
|
+
</select>
|
|
284
|
+
</div>
|
|
285
|
+
<div className="dww-field">
|
|
286
|
+
<label className="dww-field-label" htmlFor="dww-path">{t('dialog.path')}</label>
|
|
287
|
+
<div className="dww-input-row">
|
|
288
|
+
<input
|
|
289
|
+
id="dww-path"
|
|
290
|
+
className="dww-input"
|
|
291
|
+
value={pathInput}
|
|
292
|
+
placeholder={t('dialog.pathPlaceholder')}
|
|
293
|
+
disabled={opening || busy}
|
|
294
|
+
onChange={event => setPathInput(event.target.value)}
|
|
295
|
+
/>
|
|
296
|
+
<button type="button" className="dww-check-btn" disabled={opening || busy} onClick={() => void onCheck()}>
|
|
297
|
+
{t('dialog.check')}
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div className="dww-field">
|
|
302
|
+
<label className="dww-field-label" htmlFor="dww-username">{t('dialog.username')}</label>
|
|
303
|
+
<input
|
|
304
|
+
id="dww-username"
|
|
305
|
+
className="dww-input"
|
|
306
|
+
value={username}
|
|
307
|
+
placeholder={t('dialog.usernamePlaceholder')}
|
|
308
|
+
disabled={opening || busy}
|
|
309
|
+
autoComplete="off"
|
|
310
|
+
spellCheck={false}
|
|
311
|
+
onChange={event => setUsername(event.target.value)}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
<div className="dww-feedback">
|
|
315
|
+
<div className="dww-breadcrumb">{browsePath}</div>
|
|
316
|
+
<div className="dww-dirlist" onScroll={listScroll}>
|
|
317
|
+
{browsing ? <div className="dww-dir-empty">{t('dialog.loading')}</div> : (
|
|
318
|
+
listing?.parent !== null && listing !== null
|
|
319
|
+
? (
|
|
320
|
+
<button type="button" className="dww-dir-row dww-dir-row--up" onClick={onUp}>
|
|
321
|
+
<WslGlyph size={14} />
|
|
322
|
+
<span>{t('dialog.upLevel')}</span>
|
|
323
|
+
</button>
|
|
324
|
+
)
|
|
325
|
+
: null
|
|
326
|
+
)}
|
|
327
|
+
{!browsing && (children.length === 0)
|
|
328
|
+
? <div className="dww-dir-empty">{t('dialog.browseEmpty')}</div>
|
|
329
|
+
: children.map(name => (
|
|
330
|
+
<button type="button" key={name} className="dww-dir-row" onClick={() => onDrill(name)}>
|
|
331
|
+
<WslGlyph size={14} />
|
|
332
|
+
<span>{name}</span>
|
|
333
|
+
</button>
|
|
334
|
+
))}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
<div className="dww-actions">
|
|
339
|
+
<button type="button" className="dww-btn" disabled={busy} onClick={maskClick}>{t('dialog.cancel')}</button>
|
|
340
|
+
<button type="button" className="dww-btn dww-btn--primary" disabled={busy || opening} onClick={() => void onConfirm()}>
|
|
341
|
+
{busy ? t('dialog.loading') : t('dialog.confirm')}
|
|
342
|
+
</button>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export type { ApiCall }
|