@woosau/dsh-custom-env 0.1.4 → 0.1.8
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.md +86 -84
- package/README.zh-CN.md +86 -84
- package/lib/client.js +522 -105
- package/lib/index.js +18 -24
- package/lib/types/client/env-row-select.d.ts +36 -0
- package/lib/types/client/parse-env-paste.d.ts +21 -0
- package/lib/types/os-user-env/windows.d.ts +5 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,84 +1,86 @@
|
|
|
1
|
-
# dsh-custom-env
|
|
2
|
-
|
|
3
|
-
English | [中文](README.zh-CN.md)
|
|
4
|
-
|
|
5
|
-
Community DeepSeek Harness plugin for managing environment variables in **Settings → Environment**. Not an official DeepSeek AI product.
|
|
6
|
-
|
|
7
|
-
Changes apply to the running app right away, and on Windows / macOS also to the user environment so new terminals can see them. Linux user-environment sync is not supported.
|
|
8
|
-
|
|
9
|
-
[](LICENSE)
|
|
10
|
-
|
|
11
|
-
## Install
|
|
12
|
-
|
|
13
|
-
Requires DSH with the `dsh` CLI (`desktop` or `web`).
|
|
14
|
-
|
|
15
|
-
```powershell
|
|
16
|
-
dsh plugin --profile desktop add @woosau/dsh-custom-env
|
|
17
|
-
# or: dsh plugin --profile web add @woosau/dsh-custom-env
|
|
18
|
-
# or: dsh plugin --profile desktop add github:ownduck/dsh-custom-env
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
If install fails because Desktop is locking files, quit Desktop first. After install, restart DSH and open **Settings → Environment**.
|
|
22
|
-
|
|
23
|
-
```powershell
|
|
24
|
-
dsh plugin --profile desktop remove @woosau/dsh-custom-env
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## For users
|
|
28
|
-
|
|
29
|
-
1. Open **Settings → Environment**
|
|
30
|
-
2. Edit a row as a draft → **Confirm** to save, **Cancel** to discard
|
|
31
|
-
3.
|
|
32
|
-
4.
|
|
33
|
-
5.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await env?.
|
|
56
|
-
|
|
57
|
-
await env?.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
|
71
|
-
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
1
|
+
# dsh-custom-env
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
Community DeepSeek Harness plugin for managing environment variables in **Settings → Environment**. Not an official DeepSeek AI product.
|
|
6
|
+
|
|
7
|
+
Changes apply to the running app right away, and on Windows / macOS also to the user environment so new terminals can see them. Linux user-environment sync is not supported.
|
|
8
|
+
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Requires DSH with the `dsh` CLI (`desktop` or `web`).
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
dsh plugin --profile desktop add @woosau/dsh-custom-env
|
|
17
|
+
# or: dsh plugin --profile web add @woosau/dsh-custom-env
|
|
18
|
+
# or: dsh plugin --profile desktop add github:ownduck/dsh-custom-env
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If install fails because Desktop is locking files, quit Desktop first. After install, restart DSH and open **Settings → Environment**.
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
dsh plugin --profile desktop remove @woosau/dsh-custom-env
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## For users
|
|
28
|
+
|
|
29
|
+
1. Open **Settings → Environment**
|
|
30
|
+
2. Edit a row as a draft → **Confirm** to save, **Cancel** to discard
|
|
31
|
+
3. Paste `NAME=VALUE` lines into the empty row to add or update several variables at once (same names overlay existing rows as drafts)
|
|
32
|
+
4. Drag across rows to multi-select, or Ctrl/Cmd+click to toggle; Ctrl/Cmd+C copies selected rows as `NAME=VALUE` (input text selection still wins when present)
|
|
33
|
+
5. **Restore** brings back the value from when this app session started
|
|
34
|
+
6. At startup, values are injected into the Host `process.env` first; writing the user environment (registry / launchctl) is deferred off the boot path and is not broadcast. Log off or reboot before other tools need those values
|
|
35
|
+
7. Use **Install** only when shown, to wire this Host’s `dsh` command into your user PATH (Windows / macOS)
|
|
36
|
+
|
|
37
|
+
System-reserved names (such as `PATH` and names starting with `DSH_` / `XDG_`) cannot be edited here.
|
|
38
|
+
|
|
39
|
+
### When saving a value
|
|
40
|
+
|
|
41
|
+
| You save | Variable does not exist yet | Variable already exists |
|
|
42
|
+
|----------|-----------------------------|-------------------------|
|
|
43
|
+
| A non-empty value | Created | Replaced |
|
|
44
|
+
| An empty value | Created as empty | Left unchanged |
|
|
45
|
+
|
|
46
|
+
## For other plugins (Remote API)
|
|
47
|
+
|
|
48
|
+
Call the Host `customEnv` service (`ctx.get('customEnv')` or injected `ctx.customEnv`).
|
|
49
|
+
|
|
50
|
+
**Everyday use — by variable name**
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const env = ctx.get('customEnv')
|
|
54
|
+
|
|
55
|
+
await env?.setVar('MY_KEY', 'value') // create or replace
|
|
56
|
+
await env?.setVar('MY_KEY', '') // create only if missing
|
|
57
|
+
await env?.removeVar('MY_KEY')
|
|
58
|
+
|
|
59
|
+
await env?.setVars([
|
|
60
|
+
{ name: 'A', value: '1' },
|
|
61
|
+
{ name: 'B', value: '' }, // no-op if B already exists
|
|
62
|
+
])
|
|
63
|
+
await env?.removeVars(['A', 'B'])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Reserved names are rejected (batch calls fail entirely).
|
|
67
|
+
|
|
68
|
+
**Settings UI — by row id**
|
|
69
|
+
|
|
70
|
+
| Call | Purpose |
|
|
71
|
+
|------|---------|
|
|
72
|
+
| `list()` | Current variables and row ids |
|
|
73
|
+
| `upsert(id, name, value)` | Create (`id` empty) or update/rename a row |
|
|
74
|
+
| `deleteEntry(id)` | Delete that row |
|
|
75
|
+
| `restore(id)` | Restore session-start value, or remove if the row was added later |
|
|
76
|
+
|
|
77
|
+
**CLI helpers**
|
|
78
|
+
|
|
79
|
+
| Call | Purpose |
|
|
80
|
+
|------|---------|
|
|
81
|
+
| `cliStatus()` | Whether this Host’s `dsh` CLI is ready / can be installed |
|
|
82
|
+
| `cliInstall()` | Install or repair that CLI entry |
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/README.zh-CN.md
CHANGED
|
@@ -1,84 +1,86 @@
|
|
|
1
|
-
# dsh-custom-env
|
|
2
|
-
|
|
3
|
-
[English](README.md) | 中文
|
|
4
|
-
|
|
5
|
-
社区 DeepSeek Harness 插件:在 **设置 → 环境变量** 里管理环境变量。非 DeepSeek AI 官方产品。
|
|
6
|
-
|
|
7
|
-
保存后立即作用于当前应用;在 Windows / macOS 上还会写入用户环境,方便新开的终端读到。不支持同步 Linux 用户环境。
|
|
8
|
-
|
|
9
|
-
[](LICENSE)
|
|
10
|
-
|
|
11
|
-
## 安装
|
|
12
|
-
|
|
13
|
-
需已安装 DSH,并能使用 `dsh` 命令(`desktop` 或 `web`)。
|
|
14
|
-
|
|
15
|
-
```powershell
|
|
16
|
-
dsh plugin --profile desktop add @woosau/dsh-custom-env
|
|
17
|
-
# 或: dsh plugin --profile web add @woosau/dsh-custom-env
|
|
18
|
-
# 或: dsh plugin --profile desktop add github:ownduck/dsh-custom-env
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
若因 Desktop 占用文件导致安装失败,先退出 Desktop。安装后重启 DSH,打开 **设置 → 环境变量**。
|
|
22
|
-
|
|
23
|
-
```powershell
|
|
24
|
-
dsh plugin --profile desktop remove @woosau/dsh-custom-env
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## 给使用者
|
|
28
|
-
|
|
29
|
-
1. 打开 **设置 → 环境变量**
|
|
30
|
-
2. 先改草稿 → **确定** 保存,**取消** 放弃
|
|
31
|
-
3.
|
|
32
|
-
4.
|
|
33
|
-
5.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await env?.
|
|
56
|
-
|
|
57
|
-
await env?.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
|
71
|
-
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
1
|
+
# dsh-custom-env
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
社区 DeepSeek Harness 插件:在 **设置 → 环境变量** 里管理环境变量。非 DeepSeek AI 官方产品。
|
|
6
|
+
|
|
7
|
+
保存后立即作用于当前应用;在 Windows / macOS 上还会写入用户环境,方便新开的终端读到。不支持同步 Linux 用户环境。
|
|
8
|
+
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
需已安装 DSH,并能使用 `dsh` 命令(`desktop` 或 `web`)。
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
dsh plugin --profile desktop add @woosau/dsh-custom-env
|
|
17
|
+
# 或: dsh plugin --profile web add @woosau/dsh-custom-env
|
|
18
|
+
# 或: dsh plugin --profile desktop add github:ownduck/dsh-custom-env
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
若因 Desktop 占用文件导致安装失败,先退出 Desktop。安装后重启 DSH,打开 **设置 → 环境变量**。
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
dsh plugin --profile desktop remove @woosau/dsh-custom-env
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 给使用者
|
|
28
|
+
|
|
29
|
+
1. 打开 **设置 → 环境变量**
|
|
30
|
+
2. 先改草稿 → **确定** 保存,**取消** 放弃
|
|
31
|
+
3. 在空行粘贴多行 `NAME=VALUE` 可批量新增或覆盖同名变量(同名写入原行草稿)
|
|
32
|
+
4. 拖动可多选行,Ctrl/Cmd+点击切换选中;Ctrl/Cmd+C 按 `NAME=VALUE` 复制选中行(若 Input 内有文字选区则优先复制文字)
|
|
33
|
+
5. **恢复** 可回到本次应用启动时的值
|
|
34
|
+
6. 启动时先注入当前 Host 的 `process.env`;写入用户环境(注册表 / launchctl)会延后到启动路径之后,且不会广播。其它工具若要读到,请注销/重启后再开终端
|
|
35
|
+
7. 仅在出现 **安装** 时使用:把本 Host 的 `dsh` 接到用户 PATH(Windows / macOS)
|
|
36
|
+
|
|
37
|
+
系统保留名(如 `PATH`,以及 `DSH_` / `XDG_` 开头)不可在此编辑。
|
|
38
|
+
|
|
39
|
+
### 保存时怎么处理
|
|
40
|
+
|
|
41
|
+
| 你保存的内容 | 变量还不存在 | 变量已存在 |
|
|
42
|
+
|--------------|--------------|------------|
|
|
43
|
+
| 非空 | 新建 | 覆盖 |
|
|
44
|
+
| 空 | 建为空变量 | 保持原样不动 |
|
|
45
|
+
|
|
46
|
+
## 给其他插件(Remote API)
|
|
47
|
+
|
|
48
|
+
通过 Host 的 `customEnv` 服务调用(`ctx.get('customEnv')` 或已注入的 `ctx.customEnv`)。
|
|
49
|
+
|
|
50
|
+
**常用:按变量名**
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const env = ctx.get('customEnv')
|
|
54
|
+
|
|
55
|
+
await env?.setVar('MY_KEY', 'value') // 新建或覆盖
|
|
56
|
+
await env?.setVar('MY_KEY', '') // 仅当不存在时新建
|
|
57
|
+
await env?.removeVar('MY_KEY')
|
|
58
|
+
|
|
59
|
+
await env?.setVars([
|
|
60
|
+
{ name: 'A', value: '1' },
|
|
61
|
+
{ name: 'B', value: '' }, // B 已存在则跳过
|
|
62
|
+
])
|
|
63
|
+
await env?.removeVars(['A', 'B'])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
保留名会被拒绝(批量调用整批失败)。
|
|
67
|
+
|
|
68
|
+
**设置页:按行 id**
|
|
69
|
+
|
|
70
|
+
| 调用 | 用途 |
|
|
71
|
+
|------|------|
|
|
72
|
+
| `list()` | 列出当前变量与行 id |
|
|
73
|
+
| `upsert(id, name, value)` | 新建(`id` 为空)或更新/重命名一行 |
|
|
74
|
+
| `deleteEntry(id)` | 删除该行 |
|
|
75
|
+
| `restore(id)` | 恢复启动时的值;若是启动后新增的行则删除 |
|
|
76
|
+
|
|
77
|
+
**CLI**
|
|
78
|
+
|
|
79
|
+
| 调用 | 用途 |
|
|
80
|
+
|------|------|
|
|
81
|
+
| `cliStatus()` | 本 Host 的 `dsh` 是否可用 / 能否安装 |
|
|
82
|
+
| `cliInstall()` | 安装或修复该 CLI 入口 |
|
|
83
|
+
|
|
84
|
+
## 许可证
|
|
85
|
+
|
|
86
|
+
MIT
|
package/lib/client.js
CHANGED
|
@@ -553,8 +553,80 @@ function formatRemoteError(error, t) {
|
|
|
553
553
|
return t("error.generic");
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
+
// src/client/parse-env-paste.ts
|
|
557
|
+
var NAME_OK = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
558
|
+
function parseEnvPaste(text) {
|
|
559
|
+
const lastByName = /* @__PURE__ */ new Map();
|
|
560
|
+
const order = [];
|
|
561
|
+
for (const rawLine of text.split(/\r\n|\n|\r/u)) {
|
|
562
|
+
const line = rawLine.trim();
|
|
563
|
+
if (line === "") continue;
|
|
564
|
+
const eq = line.indexOf("=");
|
|
565
|
+
if (eq <= 0) continue;
|
|
566
|
+
const name = line.slice(0, eq).trim();
|
|
567
|
+
const value = line.slice(eq + 1).trim();
|
|
568
|
+
if (!NAME_OK.test(name)) continue;
|
|
569
|
+
if (!lastByName.has(name)) order.push(name);
|
|
570
|
+
lastByName.set(name, value);
|
|
571
|
+
}
|
|
572
|
+
return order.map((name) => ({ name, value: lastByName.get(name) }));
|
|
573
|
+
}
|
|
574
|
+
function formatEnvCopy(pairs) {
|
|
575
|
+
const lines = [];
|
|
576
|
+
for (const pair of pairs) {
|
|
577
|
+
const name = pair.name.trim();
|
|
578
|
+
if (name === "") continue;
|
|
579
|
+
lines.push(`${name}=${pair.value}`);
|
|
580
|
+
}
|
|
581
|
+
return lines.join("\n");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/client/env-row-select.ts
|
|
585
|
+
function isSelectionEditKey(event) {
|
|
586
|
+
return event.ctrlKey || event.metaKey;
|
|
587
|
+
}
|
|
588
|
+
function rangeIds(ordered, from, to) {
|
|
589
|
+
const a = ordered.indexOf(from);
|
|
590
|
+
const b = ordered.indexOf(to);
|
|
591
|
+
if (a < 0 || b < 0) return from === to && a >= 0 ? [from] : [];
|
|
592
|
+
const start = Math.min(a, b);
|
|
593
|
+
const end = Math.max(a, b);
|
|
594
|
+
return ordered.slice(start, end + 1);
|
|
595
|
+
}
|
|
596
|
+
function clearDomTextSelection() {
|
|
597
|
+
const selection = window.getSelection();
|
|
598
|
+
if (selection !== null && selection.rangeCount > 0) selection.removeAllRanges();
|
|
599
|
+
}
|
|
600
|
+
function hasFocusedInputTextSelection() {
|
|
601
|
+
const active = document.activeElement;
|
|
602
|
+
if (!(active instanceof HTMLInputElement) && !(active instanceof HTMLTextAreaElement)) return false;
|
|
603
|
+
const start = active.selectionStart;
|
|
604
|
+
const end = active.selectionEnd;
|
|
605
|
+
return start !== null && end !== null && start !== end;
|
|
606
|
+
}
|
|
607
|
+
function selectedEnvCopyText(selectedIds, orderedIds, drafts) {
|
|
608
|
+
if (selectedIds.size === 0) return "";
|
|
609
|
+
const pairs = orderedIds.flatMap((id) => {
|
|
610
|
+
if (!selectedIds.has(id)) return [];
|
|
611
|
+
const draft = drafts[id];
|
|
612
|
+
if (draft === void 0) return [];
|
|
613
|
+
const name = draft.name.trim();
|
|
614
|
+
if (name === "") return [];
|
|
615
|
+
return [{ name, value: draft.value }];
|
|
616
|
+
});
|
|
617
|
+
return formatEnvCopy(pairs);
|
|
618
|
+
}
|
|
619
|
+
function rowIdFromPoint(clientX, clientY) {
|
|
620
|
+
const hit = document.elementFromPoint(clientX, clientY);
|
|
621
|
+
if (!(hit instanceof Element)) return void 0;
|
|
622
|
+
const row = hit.closest("[data-env-row]");
|
|
623
|
+
if (!(row instanceof HTMLElement)) return void 0;
|
|
624
|
+
const id = row.dataset.envRow;
|
|
625
|
+
return id === void 0 || id === "" ? void 0 : id;
|
|
626
|
+
}
|
|
627
|
+
|
|
556
628
|
// src/client/EnvSettingsSection.module.css
|
|
557
|
-
var css = ".EnvSettingsSection_section {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-width: 760px;\n color: var(--dsw-alias-label-primary);\n}\n\n.EnvSettingsSection_heading {\n margin: 0;\n font-size: 18px;\n font-weight: 500;\n line-height: 1.3;\n}\n\n/* \u2014\u2014 dsh CLI: exactly two lines (label + path) \u2014\u2014 */\n.EnvSettingsSection_cli {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.EnvSettingsSection_cliHeading {\n margin: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.EnvSettingsSection_cliBody {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 28px;\n gap: 8px;\n align-items: center;\n height: 28px;\n}\n\n.EnvSettingsSection_cliPath {\n margin: 0;\n min-width: 0;\n font-size: 13px;\n line-height: 28px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.EnvSettingsSection_cliSlot {\n display: flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n}\n\n.EnvSettingsSection_cliSlot[data-empty='true'] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.EnvSettingsSection_notice,\n.EnvSettingsSection_empty {\n margin: 0;\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.EnvSettingsSection_notice[data-tone='error'] {\n color: var(--dsw-alias-label-danger, #c62828);\n}\n\n/* \u2014\u2014 variable list: CSS grid, not table (avoids Input overflow overlap) \u2014\u2014 */\n.EnvSettingsSection_list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n}\n\n.EnvSettingsSection_header,\n.EnvSettingsSection_row {\n display: grid;\n grid-template-columns: minmax(120px, 0.85fr) minmax(0, 1.55fr) 100px;\n gap: 10px;\n align-items: center;\n min-width: 0;\n}\n\n.EnvSettingsSection_header {\n padding: 0 0 2px;\n}\n\n.EnvSettingsSection_headerLabel {\n margin: 0;\n font-size: 12px;\n font-weight: 500;\n line-height: 1.3;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.EnvSettingsSection_headerActions {\n width: 100px;\n}\n\n.EnvSettingsSection_field {\n min-width: 0;\n overflow: hidden;\n}\n\n.EnvSettingsSection_fieldInput {\n display: flex;\n box-sizing: border-box;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n}\n\n.EnvSettingsSection_actions {\n display: grid;\n grid-template-columns: repeat(3, 28px);\n gap: 4px;\n align-items: center;\n justify-content: end;\n width: 100px;\n margin-left: auto;\n flex-shrink: 0;\n}\n\n.EnvSettingsSection_slot {\n display: flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n}\n\n.EnvSettingsSection_slot[data-empty='true'] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.EnvSettingsSection_iconButton {\n display: inline-flex;\n width: 28px;\n height: 28px;\n margin: 0;\n padding: 0;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n.EnvSettingsSection_iconButton:hover:not(:disabled) {\n background: var(--dsw-alias-fill-hover, rgba(127, 127, 127, 0.12));\n color: var(--dsw-alias-label-primary);\n}\n\n.EnvSettingsSection_iconButton:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n.EnvSettingsSection_iconButtonDanger:hover:not(:disabled) {\n color: var(--dsw-alias-label-danger, #c62828);\n}\n\n.EnvSettingsSection_iconButtonPrimary {\n color: var(--dsw-alias-brand-primary, #247bbf);\n}\n\n.EnvSettingsSection_restartBadge {\n display: inline-flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-label-warning, #c67c00);\n}\n\n.EnvSettingsSection_spinner {\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n to { transform: rotate(360deg); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .EnvSettingsSection_spinner {\n animation: none;\n }\n}\n";
|
|
629
|
+
var css = ".EnvSettingsSection_section {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-width: 760px;\n color: var(--dsw-alias-label-primary);\n}\n\n.EnvSettingsSection_heading {\n margin: 0;\n font-size: 18px;\n font-weight: 500;\n line-height: 1.3;\n}\n\n/* \u2014\u2014 dsh CLI: exactly two lines (label + path) \u2014\u2014 */\n.EnvSettingsSection_cli {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.EnvSettingsSection_cliHeading {\n margin: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.EnvSettingsSection_cliBody {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 28px;\n gap: 8px;\n align-items: center;\n height: 28px;\n}\n\n.EnvSettingsSection_cliPath {\n margin: 0;\n min-width: 0;\n font-size: 13px;\n line-height: 28px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.EnvSettingsSection_cliSlot {\n display: flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n}\n\n.EnvSettingsSection_cliSlot[data-empty='true'] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.EnvSettingsSection_notice,\n.EnvSettingsSection_empty {\n margin: 0;\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.EnvSettingsSection_notice[data-tone='error'] {\n color: var(--dsw-alias-label-danger, #c62828);\n}\n\n/* \u2014\u2014 variable list: CSS grid, not table (avoids Input overflow overlap) \u2014\u2014 */\n.EnvSettingsSection_list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n}\n\n.EnvSettingsSection_header,\n.EnvSettingsSection_row {\n display: grid;\n grid-template-columns: minmax(120px, 0.85fr) minmax(0, 1.55fr) 100px;\n gap: 10px;\n align-items: center;\n min-width: 0;\n}\n\n.EnvSettingsSection_row {\n padding: 4px 6px;\n margin: 0 -6px;\n user-select: none;\n}\n\n.EnvSettingsSection_row[data-selected='true'] {\n background: var(--dsw-alias-fill-hover, rgba(127, 127, 127, 0.1));\n}\n\n.EnvSettingsSection_row[data-composer='true'] {\n user-select: auto;\n}\n\n.EnvSettingsSection_header {\n padding: 0 0 2px;\n}\n\n.EnvSettingsSection_headerLabel {\n margin: 0;\n font-size: 12px;\n font-weight: 500;\n line-height: 1.3;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.EnvSettingsSection_headerActions {\n width: 100px;\n}\n\n.EnvSettingsSection_field {\n min-width: 0;\n overflow: hidden;\n}\n\n.EnvSettingsSection_fieldInput {\n display: flex;\n box-sizing: border-box;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n /* Keep unfocused inputs out of drag text-select; focused field can still edit/select. */\n user-select: none;\n}\n\n.EnvSettingsSection_field:focus-within,\n.EnvSettingsSection_field:focus-within .EnvSettingsSection_fieldInput,\n.EnvSettingsSection_row[data-composer='true'] .EnvSettingsSection_fieldInput {\n user-select: text;\n}\n\n.EnvSettingsSection_actions {\n display: grid;\n grid-template-columns: repeat(3, 28px);\n gap: 4px;\n align-items: center;\n justify-content: end;\n width: 100px;\n margin-left: auto;\n flex-shrink: 0;\n}\n\n.EnvSettingsSection_slot {\n display: flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n}\n\n.EnvSettingsSection_slot[data-empty='true'] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.EnvSettingsSection_iconButton {\n display: inline-flex;\n width: 28px;\n height: 28px;\n margin: 0;\n padding: 0;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n.EnvSettingsSection_iconButton:hover:not(:disabled) {\n background: var(--dsw-alias-fill-hover, rgba(127, 127, 127, 0.12));\n color: var(--dsw-alias-label-primary);\n}\n\n.EnvSettingsSection_iconButton:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n.EnvSettingsSection_iconButtonDanger:hover:not(:disabled) {\n color: var(--dsw-alias-label-danger, #c62828);\n}\n\n.EnvSettingsSection_iconButtonPrimary {\n color: var(--dsw-alias-brand-primary, #247bbf);\n}\n\n.EnvSettingsSection_restartBadge {\n display: inline-flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-label-warning, #c67c00);\n}\n\n.EnvSettingsSection_spinner {\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n to { transform: rotate(360deg); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .EnvSettingsSection_spinner {\n animation: none;\n }\n}\n";
|
|
558
630
|
if (typeof document !== "undefined" && !document.getElementById("dsh-custom-env-css")) {
|
|
559
631
|
const style = document.createElement("style");
|
|
560
632
|
style.id = "dsh-custom-env-css";
|
|
@@ -565,8 +637,12 @@ var EnvSettingsSection_default = { "section": "EnvSettingsSection_section", "hea
|
|
|
565
637
|
|
|
566
638
|
// src/client/EnvSettingsSection.tsx
|
|
567
639
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
568
|
-
var
|
|
569
|
-
var
|
|
640
|
+
var NAME_OK2 = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
641
|
+
var COMPOSER_ID = "";
|
|
642
|
+
var PENDING_PREFIX = "pending:";
|
|
643
|
+
function isPendingId(id) {
|
|
644
|
+
return id.startsWith(PENDING_PREFIX);
|
|
645
|
+
}
|
|
570
646
|
function IconAction({
|
|
571
647
|
label,
|
|
572
648
|
disabled,
|
|
@@ -622,9 +698,17 @@ function Field(props) {
|
|
|
622
698
|
title: props.title,
|
|
623
699
|
"aria-invalid": props.invalid || void 0,
|
|
624
700
|
"aria-label": props.label,
|
|
701
|
+
onMouseDown: (event) => {
|
|
702
|
+
if (!props.rowSelectGestures) {
|
|
703
|
+
event.stopPropagation();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (isSelectionEditKey(event)) event.preventDefault();
|
|
707
|
+
},
|
|
625
708
|
onChange: (event) => {
|
|
626
709
|
props.onChange(event.target.value);
|
|
627
|
-
}
|
|
710
|
+
},
|
|
711
|
+
onPaste: props.onPaste
|
|
628
712
|
}
|
|
629
713
|
) });
|
|
630
714
|
}
|
|
@@ -639,20 +723,57 @@ function EnvSettingsSection({
|
|
|
639
723
|
}) {
|
|
640
724
|
const [snapshot, setSnapshot] = (0, import_react.useState)();
|
|
641
725
|
const [cli, setCli] = (0, import_react.useState)();
|
|
642
|
-
const [drafts, setDrafts] = (0, import_react.useState)({ [
|
|
726
|
+
const [drafts, setDrafts] = (0, import_react.useState)({ [COMPOSER_ID]: { name: "", value: "" } });
|
|
727
|
+
const [pendingOrder, setPendingOrder] = (0, import_react.useState)([]);
|
|
643
728
|
const [busyTarget, setBusyTarget] = (0, import_react.useState)();
|
|
644
729
|
const [error, setError] = (0, import_react.useState)();
|
|
645
730
|
const [nameError, setNameError] = (0, import_react.useState)(false);
|
|
646
731
|
const [pendingRemove, setPendingRemove] = (0, import_react.useState)();
|
|
732
|
+
const [selectedIds, setSelectedIds] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
|
|
733
|
+
const pendingSeq = (0, import_react.useRef)(0);
|
|
734
|
+
const draftsRef = (0, import_react.useRef)(drafts);
|
|
735
|
+
draftsRef.current = drafts;
|
|
736
|
+
const pendingOrderRef = (0, import_react.useRef)(pendingOrder);
|
|
737
|
+
pendingOrderRef.current = pendingOrder;
|
|
738
|
+
const selectedRef = (0, import_react.useRef)(selectedIds);
|
|
739
|
+
selectedRef.current = selectedIds;
|
|
740
|
+
const anchorIdRef = (0, import_react.useRef)(void 0);
|
|
741
|
+
const dragModeRef = (0, import_react.useRef)(void 0);
|
|
742
|
+
const dragVisitedRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
743
|
+
const dragPendingCrossRef = (0, import_react.useRef)(false);
|
|
744
|
+
const pendingRemoveRef = (0, import_react.useRef)(pendingRemove);
|
|
745
|
+
pendingRemoveRef.current = pendingRemove;
|
|
647
746
|
const busy = busyTarget !== void 0;
|
|
648
747
|
const entries = snapshot?.entries ?? [];
|
|
649
748
|
const taken = new Set(entries.map((entry) => entry.name));
|
|
650
|
-
const
|
|
749
|
+
const selectableOrder = [...entries.map((entry) => entry.id), ...pendingOrder];
|
|
750
|
+
const selectableOrderRef = (0, import_react.useRef)(selectableOrder);
|
|
751
|
+
selectableOrderRef.current = selectableOrder;
|
|
752
|
+
const keepPendingDrafts = (current, dropId) => {
|
|
753
|
+
const kept = {};
|
|
754
|
+
for (const [id, draft] of Object.entries(current)) {
|
|
755
|
+
if (!isPendingId(id) || id === dropId) continue;
|
|
756
|
+
kept[id] = draft;
|
|
757
|
+
}
|
|
758
|
+
return kept;
|
|
759
|
+
};
|
|
760
|
+
const applySnapshot = (next, dropPendingId) => {
|
|
651
761
|
setSnapshot(next);
|
|
652
|
-
|
|
653
|
-
|
|
762
|
+
if (dropPendingId !== void 0) {
|
|
763
|
+
setPendingOrder((order) => order.filter((item) => item !== dropPendingId));
|
|
764
|
+
setSelectedIds((previous) => {
|
|
765
|
+
if (!previous.has(dropPendingId)) return previous;
|
|
766
|
+
const ids = new Set(previous);
|
|
767
|
+
ids.delete(dropPendingId);
|
|
768
|
+
return ids;
|
|
769
|
+
});
|
|
770
|
+
if (anchorIdRef.current === dropPendingId) anchorIdRef.current = void 0;
|
|
771
|
+
}
|
|
772
|
+
setDrafts((current) => ({
|
|
773
|
+
[COMPOSER_ID]: { name: "", value: "" },
|
|
774
|
+
...keepPendingDrafts(current, dropPendingId),
|
|
654
775
|
...Object.fromEntries(next.entries.map((entry) => [entry.id, { name: entry.name, value: entry.value }]))
|
|
655
|
-
});
|
|
776
|
+
}));
|
|
656
777
|
};
|
|
657
778
|
const setDraft = (id, patch) => {
|
|
658
779
|
setDrafts((previous) => ({
|
|
@@ -660,12 +781,46 @@ function EnvSettingsSection({
|
|
|
660
781
|
[id]: { name: patch.name ?? previous[id]?.name ?? "", value: patch.value ?? previous[id]?.value ?? "" }
|
|
661
782
|
}));
|
|
662
783
|
};
|
|
663
|
-
const
|
|
784
|
+
const dropPending = (id) => {
|
|
785
|
+
setPendingOrder((order) => order.filter((item) => item !== id));
|
|
786
|
+
setSelectedIds((previous) => {
|
|
787
|
+
if (!previous.has(id)) return previous;
|
|
788
|
+
const next = new Set(previous);
|
|
789
|
+
next.delete(id);
|
|
790
|
+
return next;
|
|
791
|
+
});
|
|
792
|
+
if (anchorIdRef.current === id) anchorIdRef.current = void 0;
|
|
793
|
+
setDrafts((previous) => {
|
|
794
|
+
const next = { ...previous };
|
|
795
|
+
delete next[id];
|
|
796
|
+
return next;
|
|
797
|
+
});
|
|
798
|
+
};
|
|
799
|
+
const clearSelection = () => {
|
|
800
|
+
setSelectedIds(/* @__PURE__ */ new Set());
|
|
801
|
+
anchorIdRef.current = void 0;
|
|
802
|
+
clearDomTextSelection();
|
|
803
|
+
};
|
|
804
|
+
const pruneSelection = (allowed) => {
|
|
805
|
+
setSelectedIds((previous) => {
|
|
806
|
+
let changed = false;
|
|
807
|
+
const next = /* @__PURE__ */ new Set();
|
|
808
|
+
for (const id of previous) {
|
|
809
|
+
if (allowed.has(id)) next.add(id);
|
|
810
|
+
else changed = true;
|
|
811
|
+
}
|
|
812
|
+
return changed ? next : previous;
|
|
813
|
+
});
|
|
814
|
+
if (anchorIdRef.current !== void 0 && !allowed.has(anchorIdRef.current)) {
|
|
815
|
+
anchorIdRef.current = void 0;
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
const run = async (target, action, dropPendingId) => {
|
|
664
819
|
if (busyTarget !== void 0) return;
|
|
665
820
|
setBusyTarget(target);
|
|
666
821
|
setError(void 0);
|
|
667
822
|
try {
|
|
668
|
-
applySnapshot(await action());
|
|
823
|
+
applySnapshot(await action(), dropPendingId);
|
|
669
824
|
} catch (err) {
|
|
670
825
|
setError(formatRemoteError(err, t));
|
|
671
826
|
} finally {
|
|
@@ -676,7 +831,14 @@ function EnvSettingsSection({
|
|
|
676
831
|
setBusyTarget("boot");
|
|
677
832
|
setError(void 0);
|
|
678
833
|
try {
|
|
679
|
-
|
|
834
|
+
setPendingOrder([]);
|
|
835
|
+
clearSelection();
|
|
836
|
+
const next = await load();
|
|
837
|
+
setSnapshot(next);
|
|
838
|
+
setDrafts({
|
|
839
|
+
[COMPOSER_ID]: { name: "", value: "" },
|
|
840
|
+
...Object.fromEntries(next.entries.map((entry) => [entry.id, { name: entry.name, value: entry.value }]))
|
|
841
|
+
});
|
|
680
842
|
try {
|
|
681
843
|
setCli(await cliStatus());
|
|
682
844
|
} catch {
|
|
@@ -691,21 +853,207 @@ function EnvSettingsSection({
|
|
|
691
853
|
(0, import_react.useEffect)(() => {
|
|
692
854
|
void refresh();
|
|
693
855
|
}, [refresh]);
|
|
694
|
-
|
|
856
|
+
(0, import_react.useEffect)(() => {
|
|
857
|
+
const allowed = new Set(selectableOrder);
|
|
858
|
+
pruneSelection(allowed);
|
|
859
|
+
}, [selectableOrder.join("\0")]);
|
|
860
|
+
const stopEditingGesture = () => {
|
|
861
|
+
clearDomTextSelection();
|
|
862
|
+
const active = document.activeElement;
|
|
863
|
+
if (active instanceof HTMLInputElement && active.closest(`.${EnvSettingsSection_default.list}`) !== null) active.blur();
|
|
864
|
+
};
|
|
865
|
+
const applyDragOverRow = (id) => {
|
|
866
|
+
if (dragModeRef.current === void 0) return;
|
|
867
|
+
if (dragModeRef.current === "toggle") {
|
|
868
|
+
if (dragVisitedRef.current.has(id)) return;
|
|
869
|
+
stopEditingGesture();
|
|
870
|
+
dragVisitedRef.current.add(id);
|
|
871
|
+
setSelectedIds((previous) => {
|
|
872
|
+
const next = new Set(previous);
|
|
873
|
+
if (next.has(id)) next.delete(id);
|
|
874
|
+
else next.add(id);
|
|
875
|
+
return next;
|
|
876
|
+
});
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (dragPendingCrossRef.current && id === anchorIdRef.current) return;
|
|
880
|
+
if (dragPendingCrossRef.current || id !== anchorIdRef.current) {
|
|
881
|
+
dragPendingCrossRef.current = false;
|
|
882
|
+
stopEditingGesture();
|
|
883
|
+
}
|
|
884
|
+
dragVisitedRef.current.add(id);
|
|
885
|
+
setSelectedIds(new Set(rangeIds(selectableOrderRef.current, anchorIdRef.current ?? id, id)));
|
|
886
|
+
};
|
|
887
|
+
(0, import_react.useEffect)(() => {
|
|
888
|
+
if (selectedIds.size > 0) clearDomTextSelection();
|
|
889
|
+
}, [selectedIds]);
|
|
890
|
+
(0, import_react.useEffect)(() => {
|
|
891
|
+
const endDrag = () => {
|
|
892
|
+
dragModeRef.current = void 0;
|
|
893
|
+
dragVisitedRef.current = /* @__PURE__ */ new Set();
|
|
894
|
+
dragPendingCrossRef.current = false;
|
|
895
|
+
};
|
|
896
|
+
const onMove = (event) => {
|
|
897
|
+
if (event.buttons !== 1 || dragModeRef.current === void 0) return;
|
|
898
|
+
const id = rowIdFromPoint(event.clientX, event.clientY);
|
|
899
|
+
if (id !== void 0) applyDragOverRow(id);
|
|
900
|
+
};
|
|
901
|
+
const onPointerDown = (event) => {
|
|
902
|
+
if (selectedRef.current.size === 0 || pendingRemoveRef.current !== void 0) return;
|
|
903
|
+
if (!(event.target instanceof Element)) return;
|
|
904
|
+
if (event.target.closest("[data-env-row]") !== null) return;
|
|
905
|
+
clearSelection();
|
|
906
|
+
};
|
|
907
|
+
const copyText = () => selectedEnvCopyText(selectedRef.current, selectableOrderRef.current, draftsRef.current);
|
|
908
|
+
const onKeyDown = (event) => {
|
|
909
|
+
if (selectedRef.current.size === 0) return;
|
|
910
|
+
if (event.key !== "c" && event.key !== "C" || !isSelectionEditKey(event) || event.altKey || event.shiftKey) return;
|
|
911
|
+
if (hasFocusedInputTextSelection()) return;
|
|
912
|
+
const text = copyText();
|
|
913
|
+
if (text === "") return;
|
|
914
|
+
event.preventDefault();
|
|
915
|
+
void navigator.clipboard.writeText(text);
|
|
916
|
+
};
|
|
917
|
+
const onCopy = (event) => {
|
|
918
|
+
if (selectedRef.current.size === 0 || hasFocusedInputTextSelection()) return;
|
|
919
|
+
const text = copyText();
|
|
920
|
+
if (text === "") return;
|
|
921
|
+
event.preventDefault();
|
|
922
|
+
event.clipboardData?.setData("text/plain", text);
|
|
923
|
+
};
|
|
924
|
+
window.addEventListener("mouseup", endDrag);
|
|
925
|
+
window.addEventListener("mousemove", onMove);
|
|
926
|
+
document.addEventListener("mousedown", onPointerDown, true);
|
|
927
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
928
|
+
document.addEventListener("copy", onCopy, true);
|
|
929
|
+
return () => {
|
|
930
|
+
window.removeEventListener("mouseup", endDrag);
|
|
931
|
+
window.removeEventListener("mousemove", onMove);
|
|
932
|
+
document.removeEventListener("mousedown", onPointerDown, true);
|
|
933
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
934
|
+
document.removeEventListener("copy", onCopy, true);
|
|
935
|
+
};
|
|
936
|
+
}, []);
|
|
937
|
+
const saveExisting = (id, entryName) => {
|
|
695
938
|
const draft = drafts[id] ?? { name: "", value: "" };
|
|
696
939
|
const name = draft.name.trim();
|
|
697
940
|
const takenByOther = name !== entryName && taken.has(name);
|
|
698
|
-
if (!
|
|
941
|
+
if (!NAME_OK2.test(name) || takenByOther) {
|
|
942
|
+
setNameError(true);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (draft.value === "") return;
|
|
946
|
+
setNameError(false);
|
|
947
|
+
void run(`row:${id}`, () => upsert(id, name, draft.value));
|
|
948
|
+
};
|
|
949
|
+
const savePending = (id) => {
|
|
950
|
+
const draft = drafts[id] ?? { name: "", value: "" };
|
|
951
|
+
const name = draft.name.trim();
|
|
952
|
+
if (!NAME_OK2.test(name) || taken.has(name)) {
|
|
699
953
|
setNameError(true);
|
|
700
954
|
return;
|
|
701
955
|
}
|
|
702
956
|
if (draft.value === "") return;
|
|
703
957
|
setNameError(false);
|
|
704
|
-
|
|
705
|
-
|
|
958
|
+
void run(`pending:${id}`, () => upsert(COMPOSER_ID, name, draft.value), id);
|
|
959
|
+
};
|
|
960
|
+
const typeIntoComposer = (patch, focus) => {
|
|
961
|
+
setNameError(false);
|
|
962
|
+
const base = draftsRef.current[COMPOSER_ID] ?? { name: "", value: "" };
|
|
963
|
+
const next = { name: patch.name ?? base.name, value: patch.value ?? base.value };
|
|
964
|
+
const id = `${PENDING_PREFIX}${++pendingSeq.current}`;
|
|
965
|
+
setDrafts((previous) => ({
|
|
966
|
+
...previous,
|
|
967
|
+
[id]: next,
|
|
968
|
+
[COMPOSER_ID]: { name: "", value: "" }
|
|
969
|
+
}));
|
|
970
|
+
setPendingOrder((order) => [...order, id]);
|
|
971
|
+
queueMicrotask(() => {
|
|
972
|
+
document.getElementById(`env-${focus}-${id}`)?.focus();
|
|
973
|
+
});
|
|
974
|
+
};
|
|
975
|
+
const appendPendingPairs = (pairs) => {
|
|
976
|
+
if (pairs.length === 0) return;
|
|
977
|
+
setNameError(false);
|
|
978
|
+
const previous = draftsRef.current;
|
|
979
|
+
const order = pendingOrderRef.current;
|
|
980
|
+
const nextDrafts = { ...previous, [COMPOSER_ID]: { name: "", value: "" } };
|
|
981
|
+
const savedByName = new Map(entries.map((entry) => [entry.name, entry.id]));
|
|
982
|
+
const existingPendingByName = /* @__PURE__ */ new Map();
|
|
983
|
+
for (const id of order) {
|
|
984
|
+
const draft = previous[id];
|
|
985
|
+
if (draft !== void 0) existingPendingByName.set(draft.name.trim(), id);
|
|
986
|
+
}
|
|
987
|
+
const added = [];
|
|
988
|
+
const dropPending2 = /* @__PURE__ */ new Set();
|
|
989
|
+
for (const pair of pairs) {
|
|
990
|
+
const savedId = savedByName.get(pair.name);
|
|
991
|
+
if (savedId !== void 0) {
|
|
992
|
+
nextDrafts[savedId] = { name: pair.name, value: pair.value };
|
|
993
|
+
const pendingDup = existingPendingByName.get(pair.name);
|
|
994
|
+
if (pendingDup !== void 0) {
|
|
995
|
+
delete nextDrafts[pendingDup];
|
|
996
|
+
dropPending2.add(pendingDup);
|
|
997
|
+
existingPendingByName.delete(pair.name);
|
|
998
|
+
}
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const reuse = existingPendingByName.get(pair.name);
|
|
1002
|
+
if (reuse !== void 0) {
|
|
1003
|
+
nextDrafts[reuse] = { name: pair.name, value: pair.value };
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
const id = `${PENDING_PREFIX}${++pendingSeq.current}`;
|
|
1007
|
+
nextDrafts[id] = { name: pair.name, value: pair.value };
|
|
1008
|
+
existingPendingByName.set(pair.name, id);
|
|
1009
|
+
added.push(id);
|
|
1010
|
+
}
|
|
1011
|
+
setDrafts(nextDrafts);
|
|
1012
|
+
if (added.length > 0 || dropPending2.size > 0) {
|
|
1013
|
+
setPendingOrder([...order.filter((id) => !dropPending2.has(id)), ...added]);
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
const onComposerPaste = (event) => {
|
|
1017
|
+
const text = event.clipboardData.getData("text");
|
|
1018
|
+
const pairs = parseEnvPaste(text);
|
|
1019
|
+
if (pairs.length === 0) return;
|
|
1020
|
+
event.preventDefault();
|
|
1021
|
+
appendPendingPairs(pairs);
|
|
706
1022
|
};
|
|
707
|
-
const
|
|
708
|
-
|
|
1023
|
+
const onRowMouseDown = (id, event) => {
|
|
1024
|
+
if (event.button !== 0) return;
|
|
1025
|
+
const target = event.target;
|
|
1026
|
+
const onInput = target?.closest("input") !== null;
|
|
1027
|
+
const onControl = target?.closest('button, a, [role="button"]') !== null;
|
|
1028
|
+
if (isSelectionEditKey(event)) {
|
|
1029
|
+
event.preventDefault();
|
|
1030
|
+
clearDomTextSelection();
|
|
1031
|
+
dragPendingCrossRef.current = false;
|
|
1032
|
+
if (event.shiftKey && anchorIdRef.current !== void 0) {
|
|
1033
|
+
const spanned = rangeIds(selectableOrder, anchorIdRef.current, id);
|
|
1034
|
+
setSelectedIds(new Set(spanned));
|
|
1035
|
+
dragModeRef.current = "range";
|
|
1036
|
+
dragVisitedRef.current = new Set(spanned);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
setSelectedIds((previous) => {
|
|
1040
|
+
const next = new Set(previous);
|
|
1041
|
+
if (next.has(id)) next.delete(id);
|
|
1042
|
+
else next.add(id);
|
|
1043
|
+
return next;
|
|
1044
|
+
});
|
|
1045
|
+
anchorIdRef.current = id;
|
|
1046
|
+
dragModeRef.current = "toggle";
|
|
1047
|
+
dragVisitedRef.current = /* @__PURE__ */ new Set([id]);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (!onInput && !onControl) event.preventDefault();
|
|
1051
|
+
anchorIdRef.current = id;
|
|
1052
|
+
dragModeRef.current = "range";
|
|
1053
|
+
dragVisitedRef.current = /* @__PURE__ */ new Set([id]);
|
|
1054
|
+
dragPendingCrossRef.current = true;
|
|
1055
|
+
};
|
|
1056
|
+
const composer = drafts[COMPOSER_ID] ?? { name: "", value: "" };
|
|
709
1057
|
const cliInstalling = busyTarget === "cli";
|
|
710
1058
|
const cliBooting = busyTarget === "boot" || cli === void 0;
|
|
711
1059
|
const showCliAction = cli?.canInstall === true || cliInstalling;
|
|
@@ -755,120 +1103,189 @@ function EnvSettingsSection({
|
|
|
755
1103
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.headerLabel, children: t("field.value") }),
|
|
756
1104
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.headerActions, "aria-hidden": "true" })
|
|
757
1105
|
] }),
|
|
758
|
-
snapshot !== void 0 && entries.length === 0 && notice === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.empty, children: t("panel.empty") }) : null,
|
|
1106
|
+
snapshot !== void 0 && entries.length === 0 && pendingOrder.length === 0 && notice === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.empty, children: t("panel.empty") }) : null,
|
|
759
1107
|
entries.map((entry) => {
|
|
760
1108
|
const draft = drafts[entry.id] ?? { name: entry.name, value: entry.value };
|
|
761
1109
|
const dirty = draft.name !== entry.name || draft.value !== entry.value;
|
|
762
1110
|
const pending = entry.kind === "added" || entry.kind === "modified";
|
|
763
1111
|
const loading = busyTarget === `row:${entry.id}`;
|
|
764
1112
|
const nextName = draft.name.trim();
|
|
765
|
-
const canConfirm =
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1113
|
+
const canConfirm = NAME_OK2.test(nextName) && draft.value !== "" && !(nextName !== entry.name && taken.has(nextName));
|
|
1114
|
+
const selected = selectedIds.has(entry.id);
|
|
1115
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1116
|
+
"div",
|
|
1117
|
+
{
|
|
1118
|
+
className: EnvSettingsSection_default.row,
|
|
1119
|
+
"data-env-row": entry.id,
|
|
1120
|
+
"data-selected": selected ? "true" : "false",
|
|
1121
|
+
"aria-selected": selected,
|
|
1122
|
+
onMouseDown: (event) => {
|
|
1123
|
+
onRowMouseDown(entry.id, event);
|
|
1124
|
+
},
|
|
1125
|
+
children: [
|
|
1126
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1127
|
+
Field,
|
|
1128
|
+
{
|
|
1129
|
+
id: `env-name-${entry.id}`,
|
|
1130
|
+
value: draft.name,
|
|
1131
|
+
disabled: busy,
|
|
1132
|
+
title: draft.name,
|
|
1133
|
+
invalid: nameError && dirty,
|
|
1134
|
+
label: t("field.name"),
|
|
1135
|
+
rowSelectGestures: true,
|
|
1136
|
+
onChange: (name) => {
|
|
1137
|
+
setNameError(false);
|
|
1138
|
+
setDraft(entry.id, { name });
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
),
|
|
1142
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1143
|
+
Field,
|
|
1144
|
+
{
|
|
1145
|
+
id: `env-value-${entry.id}`,
|
|
1146
|
+
value: draft.value,
|
|
1147
|
+
disabled: busy,
|
|
1148
|
+
title: draft.value,
|
|
1149
|
+
label: t("field.value"),
|
|
1150
|
+
rowSelectGestures: true,
|
|
1151
|
+
onChange: (value) => {
|
|
1152
|
+
setDraft(entry.id, { value });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
),
|
|
1156
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1157
|
+
RowActions,
|
|
1158
|
+
{
|
|
1159
|
+
t,
|
|
1160
|
+
busy,
|
|
1161
|
+
loading,
|
|
1162
|
+
dirty: dirty || loading,
|
|
1163
|
+
canConfirm,
|
|
1164
|
+
pending,
|
|
1165
|
+
onConfirm: () => {
|
|
1166
|
+
saveExisting(entry.id, entry.name);
|
|
1167
|
+
},
|
|
1168
|
+
onCancel: () => {
|
|
1169
|
+
setNameError(false);
|
|
1170
|
+
setDraft(entry.id, { name: entry.name, value: entry.value });
|
|
1171
|
+
},
|
|
1172
|
+
onRestore: () => {
|
|
1173
|
+
void run(`row:${entry.id}`, () => restore(entry.id));
|
|
1174
|
+
},
|
|
1175
|
+
onRemove: () => {
|
|
1176
|
+
setPendingRemove({ id: entry.id, name: entry.name });
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
)
|
|
1180
|
+
]
|
|
1181
|
+
},
|
|
1182
|
+
entry.id
|
|
1183
|
+
);
|
|
1184
|
+
}),
|
|
1185
|
+
pendingOrder.map((id) => {
|
|
1186
|
+
const draft = drafts[id] ?? { name: "", value: "" };
|
|
1187
|
+
const loading = busyTarget === `pending:${id}`;
|
|
1188
|
+
const nextName = draft.name.trim();
|
|
1189
|
+
const canConfirm = NAME_OK2.test(nextName) && draft.value !== "" && !taken.has(nextName);
|
|
1190
|
+
const selected = selectedIds.has(id);
|
|
1191
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1192
|
+
"div",
|
|
1193
|
+
{
|
|
1194
|
+
className: EnvSettingsSection_default.row,
|
|
1195
|
+
"data-env-row": id,
|
|
1196
|
+
"data-selected": selected ? "true" : "false",
|
|
1197
|
+
"aria-selected": selected,
|
|
1198
|
+
onMouseDown: (event) => {
|
|
1199
|
+
onRowMouseDown(id, event);
|
|
1200
|
+
},
|
|
1201
|
+
children: [
|
|
1202
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1203
|
+
Field,
|
|
1204
|
+
{
|
|
1205
|
+
id: `env-name-${id}`,
|
|
1206
|
+
value: draft.name,
|
|
1207
|
+
disabled: busy,
|
|
1208
|
+
title: draft.name,
|
|
1209
|
+
invalid: nameError,
|
|
1210
|
+
label: t("field.name"),
|
|
1211
|
+
rowSelectGestures: true,
|
|
1212
|
+
onChange: (name) => {
|
|
1213
|
+
setNameError(false);
|
|
1214
|
+
setDraft(id, { name });
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
),
|
|
1218
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1219
|
+
Field,
|
|
1220
|
+
{
|
|
1221
|
+
id: `env-value-${id}`,
|
|
1222
|
+
value: draft.value,
|
|
1223
|
+
disabled: busy,
|
|
1224
|
+
title: draft.value,
|
|
1225
|
+
label: t("field.value"),
|
|
1226
|
+
rowSelectGestures: true,
|
|
1227
|
+
onChange: (value) => {
|
|
1228
|
+
setDraft(id, { value });
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
),
|
|
1232
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1233
|
+
RowActions,
|
|
1234
|
+
{
|
|
1235
|
+
t,
|
|
1236
|
+
busy,
|
|
1237
|
+
loading,
|
|
1238
|
+
dirty: true,
|
|
1239
|
+
canConfirm,
|
|
1240
|
+
showRemove: false,
|
|
1241
|
+
onConfirm: () => {
|
|
1242
|
+
savePending(id);
|
|
1243
|
+
},
|
|
1244
|
+
onCancel: () => {
|
|
1245
|
+
setNameError(false);
|
|
1246
|
+
dropPending(id);
|
|
1247
|
+
},
|
|
1248
|
+
onRestore: () => void 0,
|
|
1249
|
+
onRemove: () => void 0
|
|
1250
|
+
}
|
|
1251
|
+
)
|
|
1252
|
+
]
|
|
1253
|
+
},
|
|
1254
|
+
id
|
|
1255
|
+
);
|
|
820
1256
|
}),
|
|
821
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.row, children: [
|
|
1257
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.row, "data-composer": "true", children: [
|
|
822
1258
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
823
1259
|
Field,
|
|
824
1260
|
{
|
|
825
1261
|
id: "env-new-name",
|
|
826
|
-
value:
|
|
1262
|
+
value: composer.name,
|
|
827
1263
|
disabled: busy,
|
|
828
1264
|
placeholder: t("field.namePlaceholder"),
|
|
829
|
-
title:
|
|
830
|
-
invalid: nameError,
|
|
1265
|
+
title: composer.name || t("field.namePlaceholder"),
|
|
831
1266
|
label: t("field.name"),
|
|
832
1267
|
onChange: (name) => {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1268
|
+
typeIntoComposer({ name }, "name");
|
|
1269
|
+
},
|
|
1270
|
+
onPaste: onComposerPaste
|
|
836
1271
|
}
|
|
837
1272
|
),
|
|
838
1273
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
839
1274
|
Field,
|
|
840
1275
|
{
|
|
841
1276
|
id: "env-new-value",
|
|
842
|
-
value:
|
|
1277
|
+
value: composer.value,
|
|
843
1278
|
disabled: busy,
|
|
844
1279
|
placeholder: t("field.valuePlaceholder"),
|
|
845
|
-
title:
|
|
1280
|
+
title: composer.value || t("field.valuePlaceholder"),
|
|
846
1281
|
label: t("field.value"),
|
|
847
1282
|
onChange: (value) => {
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
),
|
|
852
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
853
|
-
RowActions,
|
|
854
|
-
{
|
|
855
|
-
t,
|
|
856
|
-
busy,
|
|
857
|
-
loading: busyTarget === "new",
|
|
858
|
-
dirty: newDirty || busyTarget === "new",
|
|
859
|
-
canConfirm: NAME_OK.test(newDraft.name.trim()) && newDraft.value !== "" && !taken.has(newDraft.name.trim()),
|
|
860
|
-
showRemove: false,
|
|
861
|
-
onConfirm: () => {
|
|
862
|
-
saveRow(NEW_ID, void 0);
|
|
863
|
-
},
|
|
864
|
-
onCancel: () => {
|
|
865
|
-
setNameError(false);
|
|
866
|
-
setDraft(NEW_ID, { name: "", value: "" });
|
|
1283
|
+
typeIntoComposer({ value }, "value");
|
|
867
1284
|
},
|
|
868
|
-
|
|
869
|
-
onRemove: () => void 0
|
|
1285
|
+
onPaste: onComposerPaste
|
|
870
1286
|
}
|
|
871
|
-
)
|
|
1287
|
+
),
|
|
1288
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.actions, "aria-hidden": "true" })
|
|
872
1289
|
] })
|
|
873
1290
|
] }),
|
|
874
1291
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
package/lib/index.js
CHANGED
|
@@ -691,16 +691,12 @@ var KEY_SET_VALUE = 2;
|
|
|
691
691
|
var REG_SZ = 1;
|
|
692
692
|
var ERROR_SUCCESS = 0;
|
|
693
693
|
var ERROR_FILE_NOT_FOUND = 2;
|
|
694
|
-
var HWND_BROADCAST = 65535;
|
|
695
|
-
var WM_SETTINGCHANGE = 26;
|
|
696
|
-
var SMTO_ABORTIFHUNG = 2;
|
|
697
694
|
var bindings;
|
|
698
695
|
function loadBindings() {
|
|
699
696
|
if (bindings !== void 0) return bindings;
|
|
700
697
|
const require2 = createRequire(import.meta.url);
|
|
701
698
|
const koffi = require2("koffi");
|
|
702
699
|
const advapi32 = koffi.load("advapi32.dll");
|
|
703
|
-
const user32 = koffi.load("user32.dll");
|
|
704
700
|
bindings = {
|
|
705
701
|
openKey: advapi32.func(
|
|
706
702
|
"long __stdcall RegOpenKeyExW(uintptr_t hKey, const char16_t *subKey, uint32_t options, uint32_t samDesired, _Out_ void **result)"
|
|
@@ -714,28 +710,13 @@ function loadBindings() {
|
|
|
714
710
|
deleteValue: advapi32.func(
|
|
715
711
|
"long __stdcall RegDeleteValueW(void *hKey, const char16_t *valueName)"
|
|
716
712
|
),
|
|
717
|
-
closeKey: advapi32.func("long __stdcall RegCloseKey(void *hKey)")
|
|
718
|
-
sendMessageTimeout: user32.func(
|
|
719
|
-
"intptr_t __stdcall SendMessageTimeoutW(uintptr_t hWnd, uint32_t msg, uintptr_t wParam, const char16_t *lParam, uint32_t flags, uint32_t timeout, _Out_ uintptr_t *result)"
|
|
720
|
-
)
|
|
713
|
+
closeKey: advapi32.func("long __stdcall RegCloseKey(void *hKey)")
|
|
721
714
|
};
|
|
722
715
|
return bindings;
|
|
723
716
|
}
|
|
724
717
|
function encodeRegSz(value) {
|
|
725
718
|
return Buffer.from(`${value}\0`, "utf16le");
|
|
726
719
|
}
|
|
727
|
-
function broadcastEnvironmentChange(api) {
|
|
728
|
-
const broadcastResult = [0];
|
|
729
|
-
api.sendMessageTimeout(
|
|
730
|
-
HWND_BROADCAST,
|
|
731
|
-
WM_SETTINGCHANGE,
|
|
732
|
-
0,
|
|
733
|
-
"Environment",
|
|
734
|
-
SMTO_ABORTIFHUNG,
|
|
735
|
-
1e3,
|
|
736
|
-
broadcastResult
|
|
737
|
-
);
|
|
738
|
-
}
|
|
739
720
|
function hasWindowsUserEnv(name, platform = process.platform) {
|
|
740
721
|
if (platform !== "win32") return false;
|
|
741
722
|
const api = loadBindings();
|
|
@@ -779,7 +760,6 @@ function writeWindowsUserEnv(entries, platform = process.platform) {
|
|
|
779
760
|
} finally {
|
|
780
761
|
api.closeKey(hKey);
|
|
781
762
|
}
|
|
782
|
-
broadcastEnvironmentChange(api);
|
|
783
763
|
}
|
|
784
764
|
function removeWindowsUserEnvs(names, platform = process.platform) {
|
|
785
765
|
if (platform !== "win32" || names.length === 0) return;
|
|
@@ -800,7 +780,6 @@ function removeWindowsUserEnvs(names, platform = process.platform) {
|
|
|
800
780
|
} finally {
|
|
801
781
|
api.closeKey(hKey);
|
|
802
782
|
}
|
|
803
|
-
broadcastEnvironmentChange(api);
|
|
804
783
|
}
|
|
805
784
|
|
|
806
785
|
// src/os-user-env/index.ts
|
|
@@ -984,8 +963,11 @@ function classifyEntry(id, name, value, baselineById) {
|
|
|
984
963
|
function asMessage(error) {
|
|
985
964
|
return error instanceof Error ? error.message : String(error);
|
|
986
965
|
}
|
|
987
|
-
function
|
|
966
|
+
function applyProcessEnv(entries) {
|
|
988
967
|
for (const [key, value] of Object.entries(entries)) process.env[key] = value;
|
|
968
|
+
}
|
|
969
|
+
function applyOsUserEnv(entries) {
|
|
970
|
+
if (Object.keys(entries).length === 0) return;
|
|
989
971
|
try {
|
|
990
972
|
writeUserEnv(entries);
|
|
991
973
|
} catch (error) {
|
|
@@ -993,6 +975,10 @@ function writeLive(entries) {
|
|
|
993
975
|
`);
|
|
994
976
|
}
|
|
995
977
|
}
|
|
978
|
+
function writeLive(entries) {
|
|
979
|
+
applyProcessEnv(entries);
|
|
980
|
+
applyOsUserEnv(entries);
|
|
981
|
+
}
|
|
996
982
|
function removeLives(names) {
|
|
997
983
|
if (names.length === 0) return;
|
|
998
984
|
for (const name of names) delete process.env[name];
|
|
@@ -1023,7 +1009,15 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
|
|
|
1023
1009
|
this.bind(id, name);
|
|
1024
1010
|
this.baselineById.set(id, { name, value });
|
|
1025
1011
|
}
|
|
1026
|
-
|
|
1012
|
+
applyProcessEnv(loaded);
|
|
1013
|
+
if (Object.keys(loaded).length > 0) {
|
|
1014
|
+
const handle = setImmediate(() => {
|
|
1015
|
+
applyOsUserEnv(loaded);
|
|
1016
|
+
});
|
|
1017
|
+
ctx.effect(() => () => {
|
|
1018
|
+
clearImmediate(handle);
|
|
1019
|
+
}, "custom-env: deferred OS env sync");
|
|
1020
|
+
}
|
|
1027
1021
|
}
|
|
1028
1022
|
allocId() {
|
|
1029
1023
|
return `env-${++this.idSeq}`;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Pure helpers for Environment row multi-select and KEY=VALUE copy. */
|
|
2
|
+
/** Ctrl (Windows/Linux) or Meta (macOS) — required for click-to-toggle selection. */
|
|
3
|
+
export declare function isSelectionEditKey(event: {
|
|
4
|
+
ctrlKey: boolean;
|
|
5
|
+
metaKey: boolean;
|
|
6
|
+
}): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Inclusive id span between two anchors in list order.
|
|
9
|
+
* @param ordered - Selectable row ids in display order.
|
|
10
|
+
* @param from - Range start id.
|
|
11
|
+
* @param to - Range end id.
|
|
12
|
+
* @returns Ordered ids from the earlier index through the later; empty if either id is missing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function rangeIds(ordered: readonly string[], from: string, to: string): string[];
|
|
15
|
+
/** Drop the browser text selection (not row selection). */
|
|
16
|
+
export declare function clearDomTextSelection(): void;
|
|
17
|
+
/** True when the focused input/textarea has a non-empty character range. */
|
|
18
|
+
export declare function hasFocusedInputTextSelection(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Build clipboard text for selected rows in list order.
|
|
21
|
+
* @param selectedIds - Selected row ids.
|
|
22
|
+
* @param orderedIds - Display order (saved then pending).
|
|
23
|
+
* @param drafts - Current name/value drafts by id.
|
|
24
|
+
* @returns `NAME=VALUE` lines, or empty when nothing usable.
|
|
25
|
+
*/
|
|
26
|
+
export declare function selectedEnvCopyText(selectedIds: ReadonlySet<string>, orderedIds: readonly string[], drafts: Readonly<Record<string, {
|
|
27
|
+
name: string;
|
|
28
|
+
value: string;
|
|
29
|
+
}>>): string;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the env row under a client point, if any.
|
|
32
|
+
* @param clientX - Viewport X.
|
|
33
|
+
* @param clientY - Viewport Y.
|
|
34
|
+
* @returns `data-env-row` id, or undefined.
|
|
35
|
+
*/
|
|
36
|
+
export declare function rowIdFromPoint(clientX: number, clientY: number): string | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Parse clipboard text into environment name/value pairs for Settings paste. */
|
|
2
|
+
/** One parsed assignment from a paste. */
|
|
3
|
+
export interface ParsedEnvPair {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly value: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse multiline `NAME=VALUE` paste text into pairs.
|
|
9
|
+
* Trims lines and spaces around `=`; splits on the first `=`; skips blanks and invalid names.
|
|
10
|
+
* Duplicate names in one paste keep the last value.
|
|
11
|
+
* @param text - Clipboard text.
|
|
12
|
+
* @returns Ordered unique pairs (last duplicate wins).
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseEnvPaste(text: string): ParsedEnvPair[];
|
|
15
|
+
/**
|
|
16
|
+
* Format pairs for clipboard copy (one `NAME=VALUE` per line).
|
|
17
|
+
* Skips empty names. Does not trim values beyond what the caller provides.
|
|
18
|
+
* @param pairs - Ordered name/value pairs.
|
|
19
|
+
* @returns Clipboard text; empty string when no usable pairs.
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatEnvCopy(pairs: readonly ParsedEnvPair[]): string;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persist or delete variables in the Windows user environment (HKCU\\Environment)
|
|
3
|
-
* via koffi + advapi32
|
|
3
|
+
* via koffi + advapi32 — no WM_SETTINGCHANGE broadcast (Host already updates process.env).
|
|
4
4
|
*/
|
|
5
|
-
/** Win32 registry
|
|
5
|
+
/** Win32 registry calls used to persist user environment values. */
|
|
6
6
|
export interface WindowsUserEnvBindings {
|
|
7
7
|
readonly openKey: (hKey: number, subKey: string, options: number, samDesired: number, result: [unknown]) => number;
|
|
8
8
|
readonly queryValue: (hKey: unknown, valueName: string, reserved: unknown, type: [number] | null, data: Buffer | null, cbData: [number]) => number;
|
|
9
9
|
readonly setValue: (hKey: unknown, valueName: string, reserved: number, type: number, data: Buffer, cbData: number) => number;
|
|
10
10
|
readonly deleteValue: (hKey: unknown, valueName: string) => number;
|
|
11
11
|
readonly closeKey: (hKey: unknown) => number;
|
|
12
|
-
readonly sendMessageTimeout: (hWnd: number, msg: number, wParam: number, lParam: string, flags: number, timeoutMs: number, result: [number]) => number;
|
|
13
12
|
}
|
|
14
13
|
/**
|
|
15
14
|
* Install FFI bindings for tests (skips loading koffi).
|
|
@@ -30,14 +29,14 @@ export declare function encodeRegSz(value: string): Buffer;
|
|
|
30
29
|
*/
|
|
31
30
|
export declare function hasWindowsUserEnv(name: string, platform?: NodeJS.Platform): boolean;
|
|
32
31
|
/**
|
|
33
|
-
* Write entries into HKCU\\Environment
|
|
32
|
+
* Write entries into HKCU\\Environment (no desktop-wide Environment broadcast).
|
|
34
33
|
* No-op on non-Windows hosts or when `entries` is empty.
|
|
35
34
|
* @param entries - Variable map to persist.
|
|
36
35
|
* @param platform - Override for tests; defaults to `process.platform`.
|
|
37
36
|
*/
|
|
38
37
|
export declare function writeWindowsUserEnv(entries: Readonly<Record<string, string>>, platform?: NodeJS.Platform): void;
|
|
39
38
|
/**
|
|
40
|
-
* Delete many user environment values from HKCU\\Environment in one open
|
|
39
|
+
* Delete many user environment values from HKCU\\Environment in one open.
|
|
41
40
|
* Missing values are ignored.
|
|
42
41
|
* @param names - Variable names to remove.
|
|
43
42
|
* @param platform - Override for tests; defaults to `process.platform`.
|
|
@@ -46,7 +45,7 @@ export declare function removeWindowsUserEnvs(names: readonly string[], platform
|
|
|
46
45
|
/**
|
|
47
46
|
* Delete one user environment value from HKCU\\Environment.
|
|
48
47
|
* Missing values are ignored.
|
|
49
|
-
* @param name - Variable name to
|
|
48
|
+
* @param name - Variable name to delete.
|
|
50
49
|
* @param platform - Override for tests; defaults to `process.platform`.
|
|
51
50
|
*/
|
|
52
51
|
export declare function removeWindowsUserEnv(name: string, platform?: NodeJS.Platform): void;
|