vde-layout 1.0.0 → 1.0.2
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 +12 -8
- package/dist/index.mjs +46 -32
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ pnpm run ci
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## Quick Start
|
|
33
|
-
1. Create a YAML file at `~/.config/vde/layout.yml` (
|
|
33
|
+
1. Create a YAML file at `~/.config/vde/layout/config.yml` (legacy `~/.config/vde/layout.yml` is also supported; see "Configuration Search Order").
|
|
34
34
|
2. Paste a preset definition:
|
|
35
35
|
```yaml
|
|
36
36
|
presets:
|
|
@@ -73,7 +73,7 @@ pnpm run ci
|
|
|
73
73
|
- `vde-layout dev --backend wezterm` - Use the WezTerm backend (defaults to `tmux` when omitted).
|
|
74
74
|
- `vde-layout dev --current-window` - Reuse the current tmux window (or active WezTerm tab) after confirming that other panes can be closed.
|
|
75
75
|
- `vde-layout dev --new-window` - Force creation of a new tmux window or WezTerm tab even when presets or defaults request reuse.
|
|
76
|
-
- `vde-layout --config /path/to/
|
|
76
|
+
- `vde-layout --config /path/to/config.yml` - Load presets from a specific file.
|
|
77
77
|
- `vde-layout --help` - Show usage.
|
|
78
78
|
- `vde-layout --version` / `vde-layout -v` - Print package version.
|
|
79
79
|
|
|
@@ -88,12 +88,16 @@ vde-layout resolves backends in the following order: CLI flag (`--backend`), pre
|
|
|
88
88
|
- `--new-window` spawns a new tab in the active window when one is available, otherwise creates a fresh window.
|
|
89
89
|
|
|
90
90
|
## Configuration Search Order
|
|
91
|
-
When no `--config` flag is provided, vde-layout
|
|
92
|
-
1.
|
|
93
|
-
2. `$
|
|
94
|
-
3.
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
When no `--config` flag is provided, vde-layout checks candidate files in this order for `findConfigFile()`:
|
|
92
|
+
1. `<project-root>/.vde/layout.yml` (discovered by walking up from the current directory).
|
|
93
|
+
2. `$VDE_CONFIG_PATH/layout.yml` (if `VDE_CONFIG_PATH` is set).
|
|
94
|
+
3. `$XDG_CONFIG_HOME/vde/layout/config.yml` (or `~/.config/vde/layout/config.yml` when `XDG_CONFIG_HOME` is unset).
|
|
95
|
+
4. `$XDG_CONFIG_HOME/vde/layout.yml` fallback (or `~/.config/vde/layout.yml`).
|
|
96
|
+
|
|
97
|
+
For `loadConfig()`, vde-layout merges shared scopes first and project scope last:
|
|
98
|
+
1. `$VDE_CONFIG_PATH/layout.yml`
|
|
99
|
+
2. XDG scope (`.../vde/layout/config.yml` or fallback `.../vde/layout.yml`; first existing file only)
|
|
100
|
+
3. `<project-root>/.vde/layout.yml`
|
|
97
101
|
|
|
98
102
|
## Preset Structure
|
|
99
103
|
Each preset is an object under the `presets` key:
|
package/dist/index.mjs
CHANGED
|
@@ -248,7 +248,8 @@ const createConfigLoader = (options = {}) => {
|
|
|
248
248
|
const candidates = [];
|
|
249
249
|
const projectCandidate = findProjectConfigCandidate();
|
|
250
250
|
if (projectCandidate !== null) candidates.push(projectCandidate);
|
|
251
|
-
|
|
251
|
+
const defaultSearchPathGroups = buildDefaultSearchPathGroups();
|
|
252
|
+
candidates.push(...flattenSearchPathGroups(defaultSearchPathGroups));
|
|
252
253
|
return [...new Set(candidates)];
|
|
253
254
|
};
|
|
254
255
|
const loadConfig = async () => {
|
|
@@ -258,16 +259,16 @@ const createConfigLoader = (options = {}) => {
|
|
|
258
259
|
return validateYAML(await safeReadFile(filePath));
|
|
259
260
|
}
|
|
260
261
|
const searchPaths = computeCachedSearchPaths();
|
|
261
|
-
const
|
|
262
|
-
if (existingPaths.length === 0) throw createConfigError("Configuration file not found", ErrorCodes.CONFIG_NOT_FOUND, { searchPaths });
|
|
262
|
+
const globalPaths = await resolveFirstExistingPaths(buildDefaultSearchPathGroups());
|
|
263
263
|
const projectPath = findProjectConfigCandidate();
|
|
264
|
-
const
|
|
264
|
+
const projectConfigExists = projectPath !== null ? await fs.pathExists(projectPath) : false;
|
|
265
|
+
if (globalPaths.length === 0 && !projectConfigExists) throw createConfigError("Configuration file not found", ErrorCodes.CONFIG_NOT_FOUND, { searchPaths });
|
|
265
266
|
let mergedConfig = { presets: {} };
|
|
266
267
|
for (const globalPath of globalPaths) {
|
|
267
268
|
const config = validateYAML(await safeReadFile(globalPath));
|
|
268
269
|
mergedConfig = mergeConfigs(mergedConfig, config, emitWarning);
|
|
269
270
|
}
|
|
270
|
-
if (projectPath !== null &&
|
|
271
|
+
if (projectPath !== null && projectConfigExists) {
|
|
271
272
|
const config = validateYAML(await safeReadFile(projectPath));
|
|
272
273
|
mergedConfig = mergeConfigs(mergedConfig, config, emitWarning);
|
|
273
274
|
}
|
|
@@ -287,15 +288,30 @@ const createConfigLoader = (options = {}) => {
|
|
|
287
288
|
getSearchPaths: () => computeCachedSearchPaths()
|
|
288
289
|
};
|
|
289
290
|
};
|
|
290
|
-
const
|
|
291
|
-
const
|
|
291
|
+
const buildDefaultSearchPathGroups = () => {
|
|
292
|
+
const pathGroups = [];
|
|
292
293
|
const vdeConfigPath = process.env.VDE_CONFIG_PATH;
|
|
293
|
-
if (vdeConfigPath !== void 0)
|
|
294
|
+
if (vdeConfigPath !== void 0) pathGroups.push([path.join(vdeConfigPath, "layout.yml")]);
|
|
294
295
|
const homeDir = process.env.HOME ?? os.homedir();
|
|
295
296
|
const xdgConfigHome = process.env.XDG_CONFIG_HOME ?? path.join(homeDir, ".config");
|
|
296
|
-
|
|
297
|
+
pathGroups.push([path.join(xdgConfigHome, "vde", "layout", "config.yml"), path.join(xdgConfigHome, "vde", "layout.yml")]);
|
|
298
|
+
return pathGroups.map((group) => [...new Set(group)]);
|
|
299
|
+
};
|
|
300
|
+
const flattenSearchPathGroups = (pathGroups) => {
|
|
301
|
+
const paths = [];
|
|
302
|
+
for (const group of pathGroups) paths.push(...group);
|
|
297
303
|
return [...new Set(paths)];
|
|
298
304
|
};
|
|
305
|
+
const resolveFirstExistingPaths = async (pathGroups) => {
|
|
306
|
+
const existingPaths = await Promise.all(pathGroups.map(async (group) => findFirstExisting(group)));
|
|
307
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
308
|
+
const resolvedPaths = [];
|
|
309
|
+
for (const existingPath of existingPaths) if (existingPath !== null && !seenPaths.has(existingPath)) {
|
|
310
|
+
seenPaths.add(existingPath);
|
|
311
|
+
resolvedPaths.push(existingPath);
|
|
312
|
+
}
|
|
313
|
+
return resolvedPaths;
|
|
314
|
+
};
|
|
299
315
|
const findProjectConfigCandidate = () => {
|
|
300
316
|
let currentDir = process.cwd();
|
|
301
317
|
const { root } = path.parse(currentDir);
|
|
@@ -313,11 +329,6 @@ const findFirstExisting = async (paths) => {
|
|
|
313
329
|
for (const candidate of paths) if (await fs.pathExists(candidate)) return candidate;
|
|
314
330
|
return null;
|
|
315
331
|
};
|
|
316
|
-
const filterExistingPaths = async (paths) => {
|
|
317
|
-
const existing = [];
|
|
318
|
-
for (const candidate of paths) if (await fs.pathExists(candidate)) existing.push(candidate);
|
|
319
|
-
return existing;
|
|
320
|
-
};
|
|
321
332
|
const safeReadFile = async (filePath) => {
|
|
322
333
|
try {
|
|
323
334
|
return await fs.readFile(filePath, "utf8");
|
|
@@ -630,14 +641,14 @@ const sanitizeLayoutForSchemaValidation = (node) => {
|
|
|
630
641
|
};
|
|
631
642
|
};
|
|
632
643
|
const convertLayoutIssueToCompileError = ({ issue, source, basePath, layout }) => {
|
|
633
|
-
if (issue
|
|
644
|
+
if (isMissingArrayIssue(issue, "panes")) return compileError("LAYOUT_PANES_MISSING", {
|
|
634
645
|
source,
|
|
635
|
-
message: "panes array is missing",
|
|
646
|
+
message: "panes array is missing or empty",
|
|
636
647
|
path: `${basePath}.panes`
|
|
637
648
|
});
|
|
638
|
-
if (issue
|
|
649
|
+
if (isMissingArrayIssue(issue, "ratio")) return compileError("LAYOUT_RATIO_MISSING", {
|
|
639
650
|
source,
|
|
640
|
-
message: "ratio array is missing",
|
|
651
|
+
message: "ratio array is missing or empty",
|
|
641
652
|
path: `${basePath}.ratio`
|
|
642
653
|
});
|
|
643
654
|
if (issue.path.includes("type")) return compileError("LAYOUT_INVALID_ORIENTATION", {
|
|
@@ -668,6 +679,10 @@ const convertLayoutIssueToCompileError = ({ issue, source, basePath, layout }) =
|
|
|
668
679
|
}
|
|
669
680
|
});
|
|
670
681
|
};
|
|
682
|
+
const isMissingArrayIssue = (issue, field) => {
|
|
683
|
+
if (issue.path.length !== 1 || issue.path[0] !== field) return false;
|
|
684
|
+
return issue.code === "invalid_type" || issue.code === "too_small" && issue.type === "array";
|
|
685
|
+
};
|
|
671
686
|
const getRatioLengthDetails = (layout) => {
|
|
672
687
|
if (!isRecord(layout)) return;
|
|
673
688
|
const ratio = layout.ratio;
|
|
@@ -1264,34 +1279,33 @@ const resolvePaneMapping = (paneMap, virtualId) => {
|
|
|
1264
1279
|
|
|
1265
1280
|
//#endregion
|
|
1266
1281
|
//#region src/executor/split-step.ts
|
|
1267
|
-
const
|
|
1282
|
+
const asSplitStep = (step, field) => {
|
|
1268
1283
|
if (step.kind !== "split") throw createCoreError("execution", {
|
|
1269
1284
|
code: ErrorCodes.INVALID_PLAN,
|
|
1270
|
-
message:
|
|
1285
|
+
message: `Non-split step cannot resolve split ${field}`,
|
|
1271
1286
|
path: step.id,
|
|
1272
1287
|
details: { kind: step.kind }
|
|
1273
1288
|
});
|
|
1274
|
-
|
|
1289
|
+
return step;
|
|
1290
|
+
};
|
|
1291
|
+
const resolveSplitOrientation = (step) => {
|
|
1292
|
+
const splitStep = asSplitStep(step, "orientation");
|
|
1293
|
+
if (splitStep.orientation === "horizontal" || splitStep.orientation === "vertical") return splitStep.orientation;
|
|
1275
1294
|
throw createCoreError("execution", {
|
|
1276
1295
|
code: ErrorCodes.INVALID_PLAN,
|
|
1277
1296
|
message: "Split step missing orientation metadata",
|
|
1278
|
-
path:
|
|
1279
|
-
details: { orientation:
|
|
1297
|
+
path: splitStep.id,
|
|
1298
|
+
details: { orientation: splitStep.orientation }
|
|
1280
1299
|
});
|
|
1281
1300
|
};
|
|
1282
1301
|
const resolveSplitPercentage = (step) => {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
message: "Non-split step cannot resolve split percentage",
|
|
1286
|
-
path: step.id,
|
|
1287
|
-
details: { kind: step.kind }
|
|
1288
|
-
});
|
|
1289
|
-
if (typeof step.percentage === "number" && Number.isFinite(step.percentage)) return String(clampSplitPercentage(step.percentage));
|
|
1302
|
+
const splitStep = asSplitStep(step, "percentage");
|
|
1303
|
+
if (typeof splitStep.percentage === "number" && Number.isFinite(splitStep.percentage)) return String(clampSplitPercentage(splitStep.percentage));
|
|
1290
1304
|
throw createCoreError("execution", {
|
|
1291
1305
|
code: ErrorCodes.INVALID_PLAN,
|
|
1292
1306
|
message: "Split step missing percentage metadata",
|
|
1293
|
-
path:
|
|
1294
|
-
details: { percentage:
|
|
1307
|
+
path: splitStep.id,
|
|
1308
|
+
details: { percentage: splitStep.percentage }
|
|
1295
1309
|
});
|
|
1296
1310
|
};
|
|
1297
1311
|
const clampSplitPercentage = (value) => {
|