@carlesandres/house 0.7.0 → 0.8.0
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/CHANGELOG.md +11 -1
- package/dist/index.js +385 -196
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@ The publish workflow (`.github/workflows/publish.yml`) runs on the `release: pub
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.0] — 2026-08-30
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `@house/options` specs may declare `footer: { icon, activate? }` so House can derive clickable footer controls from the catalog; wrap's `W` indicator now comes from that opt-in instead of a hard-coded Browser entry.
|
|
14
|
+
- Footer `T` and `O` controls cycle theme and File Navigator browse order one step forward (with a short notice), so multi-value options are adjustable without leaving the keyboard-or-click chrome.
|
|
15
|
+
- Choice footer controls show a declared short abbreviation of the current value (for example `op` / `re`) instead of a fixed letter, so the chip itself reflects state between Activate clicks.
|
|
16
|
+
|
|
17
|
+
|
|
9
18
|
## [0.7.0] — 2026-08-30
|
|
10
19
|
|
|
11
20
|
### Added
|
|
@@ -433,7 +442,8 @@ The v1 MVP, published as `@carlesandres/openmdr` on npm.
|
|
|
433
442
|
|
|
434
443
|
Search, stdin, URL fetching, cross-file link following, `$EDITOR` hand-off, syntax highlighting, persistent config, OS-appearance auto-detect, single-binary distribution (issue [#2](https://github.com/carlesandres/openmdr/issues/2)), Homebrew tap. All tracked.
|
|
435
444
|
|
|
436
|
-
[Unreleased]: https://github.com/carlesandres/house/compare/v0.
|
|
445
|
+
[Unreleased]: https://github.com/carlesandres/house/compare/v0.8.0...HEAD
|
|
446
|
+
[0.8.0]: https://github.com/carlesandres/house/compare/v0.7.0...v0.8.0
|
|
437
447
|
[0.7.0]: https://github.com/carlesandres/house/compare/v0.6.0...v0.7.0
|
|
438
448
|
[0.6.0]: https://github.com/carlesandres/house/compare/v0.5.2...v0.6.0
|
|
439
449
|
[0.5.2]: https://github.com/carlesandres/house/compare/v0.5.1...v0.5.2
|
package/dist/index.js
CHANGED
|
@@ -3066,7 +3066,7 @@ import { useState as useState6 } from "react";
|
|
|
3066
3066
|
// package.json
|
|
3067
3067
|
var package_default = {
|
|
3068
3068
|
name: "@carlesandres/house",
|
|
3069
|
-
version: "0.
|
|
3069
|
+
version: "0.8.0",
|
|
3070
3070
|
description: "TUI-first markdown reader on opentui",
|
|
3071
3071
|
type: "module",
|
|
3072
3072
|
license: "MIT",
|
|
@@ -3162,6 +3162,279 @@ var package_default = {
|
|
|
3162
3162
|
// src/Browser.tsx
|
|
3163
3163
|
import { join as join4 } from "path";
|
|
3164
3164
|
import { SyntaxStyle } from "@opentui/core";
|
|
3165
|
+
|
|
3166
|
+
// ../../packages/options/src/footer.ts
|
|
3167
|
+
var footerActivate = (spec) => {
|
|
3168
|
+
if (spec.footer?.activate !== undefined)
|
|
3169
|
+
return spec.footer.activate;
|
|
3170
|
+
if (spec.type === "boolean")
|
|
3171
|
+
return "toggle";
|
|
3172
|
+
if (spec.type === "string" && spec.choices !== undefined && spec.choices.length > 0) {
|
|
3173
|
+
return "cycle";
|
|
3174
|
+
}
|
|
3175
|
+
return;
|
|
3176
|
+
};
|
|
3177
|
+
var footerKeys = (specs) => {
|
|
3178
|
+
const keys = [];
|
|
3179
|
+
for (const key of Object.keys(specs)) {
|
|
3180
|
+
if (specs[key]?.footer !== undefined)
|
|
3181
|
+
keys.push(key);
|
|
3182
|
+
}
|
|
3183
|
+
return keys;
|
|
3184
|
+
};
|
|
3185
|
+
var nextFooterValue = (spec, current) => {
|
|
3186
|
+
const activate = footerActivate(spec);
|
|
3187
|
+
if (activate === "toggle") {
|
|
3188
|
+
if (spec.type !== "boolean") {
|
|
3189
|
+
throw new Error('footer activate "toggle" requires a boolean option');
|
|
3190
|
+
}
|
|
3191
|
+
return !current;
|
|
3192
|
+
}
|
|
3193
|
+
if (activate === "cycle") {
|
|
3194
|
+
if (spec.type !== "string" || spec.choices === undefined || spec.choices.length === 0) {
|
|
3195
|
+
throw new Error('footer activate "cycle" requires a string option with choices');
|
|
3196
|
+
}
|
|
3197
|
+
const choices = spec.choices;
|
|
3198
|
+
const currentIndex = choices.indexOf(current);
|
|
3199
|
+
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % choices.length;
|
|
3200
|
+
return choices[nextIndex];
|
|
3201
|
+
}
|
|
3202
|
+
throw new Error("option has no footer activate strategy");
|
|
3203
|
+
};
|
|
3204
|
+
var footerControlActive = (spec, value) => {
|
|
3205
|
+
if (spec.type === "boolean")
|
|
3206
|
+
return Boolean(value);
|
|
3207
|
+
return true;
|
|
3208
|
+
};
|
|
3209
|
+
var footerControlGlyph = (spec, value) => {
|
|
3210
|
+
const footer = spec.footer;
|
|
3211
|
+
if (footer === undefined) {
|
|
3212
|
+
throw new Error("footerControlGlyph requires a footer-opted spec");
|
|
3213
|
+
}
|
|
3214
|
+
if (typeof value === "string" && footer.labels !== undefined) {
|
|
3215
|
+
const label = footer.labels[value];
|
|
3216
|
+
if (label !== undefined && label.length > 0)
|
|
3217
|
+
return label;
|
|
3218
|
+
}
|
|
3219
|
+
return footer.icon;
|
|
3220
|
+
};
|
|
3221
|
+
var validateFooterSpecs = (specs) => {
|
|
3222
|
+
for (const key of Object.keys(specs)) {
|
|
3223
|
+
const spec = specs[key];
|
|
3224
|
+
const footer = spec.footer;
|
|
3225
|
+
if (footer === undefined)
|
|
3226
|
+
continue;
|
|
3227
|
+
if (footer.icon.length === 0) {
|
|
3228
|
+
throw new Error(`option ${JSON.stringify(key)}: footer.icon must be non-empty`);
|
|
3229
|
+
}
|
|
3230
|
+
const activate = footerActivate(spec);
|
|
3231
|
+
if (activate === undefined) {
|
|
3232
|
+
throw new Error(`option ${JSON.stringify(key)}: footer requires activate "toggle" or "cycle" ` + "(booleans toggle; strings with choices cycle; other types need an explicit strategy)");
|
|
3233
|
+
}
|
|
3234
|
+
if (activate === "toggle" && spec.type !== "boolean") {
|
|
3235
|
+
throw new Error(`option ${JSON.stringify(key)}: footer activate "toggle" requires type "boolean"`);
|
|
3236
|
+
}
|
|
3237
|
+
if (activate === "cycle") {
|
|
3238
|
+
if (spec.type !== "string" || spec.choices === undefined || spec.choices.length === 0) {
|
|
3239
|
+
throw new Error(`option ${JSON.stringify(key)}: footer activate "cycle" requires string choices`);
|
|
3240
|
+
}
|
|
3241
|
+
const labels = footer.labels;
|
|
3242
|
+
if (labels === undefined) {
|
|
3243
|
+
throw new Error(`option ${JSON.stringify(key)}: footer.labels is required for choice options`);
|
|
3244
|
+
}
|
|
3245
|
+
for (const choice of spec.choices) {
|
|
3246
|
+
const label = labels[choice];
|
|
3247
|
+
if (label === undefined || label.length === 0) {
|
|
3248
|
+
throw new Error(`option ${JSON.stringify(key)}: footer.labels missing entry for ${JSON.stringify(choice)}`);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
|
|
3255
|
+
// ../../packages/options/src/decode.ts
|
|
3256
|
+
var isPresent = (value) => value !== undefined && value !== null;
|
|
3257
|
+
var stringifyReceived = (value) => {
|
|
3258
|
+
if (typeof value === "string")
|
|
3259
|
+
return JSON.stringify(value);
|
|
3260
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
3261
|
+
return JSON.stringify(value);
|
|
3262
|
+
if (value === null)
|
|
3263
|
+
return "null";
|
|
3264
|
+
return typeof value;
|
|
3265
|
+
};
|
|
3266
|
+
var formatResolveError = (error, filePath) => {
|
|
3267
|
+
if (error.layer === "file") {
|
|
3268
|
+
const where = filePath === undefined ? "" : ` in ${filePath}`;
|
|
3269
|
+
return `invalid value for ${error.key}${where}: ${error.message}`;
|
|
3270
|
+
}
|
|
3271
|
+
return `${error.key}: ${error.message}, got ${stringifyReceived(error.received)}`;
|
|
3272
|
+
};
|
|
3273
|
+
var fail = (key, layer, message, received) => ({
|
|
3274
|
+
key,
|
|
3275
|
+
layer,
|
|
3276
|
+
message,
|
|
3277
|
+
received
|
|
3278
|
+
});
|
|
3279
|
+
var booleanMessage = "expected true or false";
|
|
3280
|
+
var numberMessage = (spec) => {
|
|
3281
|
+
if (spec.integer === true && spec.min === 1 && spec.max === undefined) {
|
|
3282
|
+
return "expected a positive integer";
|
|
3283
|
+
}
|
|
3284
|
+
if (spec.integer === true)
|
|
3285
|
+
return "expected an integer";
|
|
3286
|
+
return "expected a number";
|
|
3287
|
+
};
|
|
3288
|
+
var inRange = (value, spec) => {
|
|
3289
|
+
if (spec.min !== undefined && value < spec.min)
|
|
3290
|
+
return false;
|
|
3291
|
+
if (spec.max !== undefined && value > spec.max)
|
|
3292
|
+
return false;
|
|
3293
|
+
if (spec.integer === true && !Number.isSafeInteger(value))
|
|
3294
|
+
return false;
|
|
3295
|
+
if (!Number.isFinite(value))
|
|
3296
|
+
return false;
|
|
3297
|
+
return true;
|
|
3298
|
+
};
|
|
3299
|
+
var decodeBoolean = (key, layer, value) => {
|
|
3300
|
+
if (layer === "env") {
|
|
3301
|
+
if (value === "true")
|
|
3302
|
+
return { ok: true, value: true };
|
|
3303
|
+
if (value === "false")
|
|
3304
|
+
return { ok: true, value: false };
|
|
3305
|
+
return { ok: false, error: fail(key, layer, booleanMessage, value) };
|
|
3306
|
+
}
|
|
3307
|
+
if (typeof value === "boolean")
|
|
3308
|
+
return { ok: true, value };
|
|
3309
|
+
return { ok: false, error: fail(key, layer, booleanMessage, value) };
|
|
3310
|
+
};
|
|
3311
|
+
var decodeNumber = (key, layer, value, spec) => {
|
|
3312
|
+
const message = numberMessage(spec);
|
|
3313
|
+
if (layer === "env") {
|
|
3314
|
+
if (typeof value !== "string" || !/^-?\d+(\.\d+)?$/.test(value)) {
|
|
3315
|
+
return { ok: false, error: fail(key, layer, message, value) };
|
|
3316
|
+
}
|
|
3317
|
+
if (spec.integer === true && !/^-?\d+$/.test(value)) {
|
|
3318
|
+
return { ok: false, error: fail(key, layer, message, value) };
|
|
3319
|
+
}
|
|
3320
|
+
const parsed = spec.integer === true ? Number.parseInt(value, 10) : Number.parseFloat(value);
|
|
3321
|
+
if (!inRange(parsed, spec))
|
|
3322
|
+
return { ok: false, error: fail(key, layer, message, value) };
|
|
3323
|
+
return { ok: true, value: parsed };
|
|
3324
|
+
}
|
|
3325
|
+
if (typeof value !== "number" || !inRange(value, spec)) {
|
|
3326
|
+
return { ok: false, error: fail(key, layer, message, value) };
|
|
3327
|
+
}
|
|
3328
|
+
return { ok: true, value };
|
|
3329
|
+
};
|
|
3330
|
+
var decodeString = (key, layer, value, spec) => {
|
|
3331
|
+
if (typeof value !== "string") {
|
|
3332
|
+
const message = spec.choices === undefined ? "expected a string" : `expected one of ${spec.choices.join(", ")}`;
|
|
3333
|
+
return { ok: false, error: fail(key, layer, message, value) };
|
|
3334
|
+
}
|
|
3335
|
+
if (spec.choices !== undefined && !spec.choices.includes(value)) {
|
|
3336
|
+
return {
|
|
3337
|
+
ok: false,
|
|
3338
|
+
error: fail(key, layer, `expected one of ${spec.choices.join(", ")}`, value)
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
return { ok: true, value };
|
|
3342
|
+
};
|
|
3343
|
+
var decodeValue = (key, spec, layer, value) => {
|
|
3344
|
+
if (spec.type === "boolean")
|
|
3345
|
+
return decodeBoolean(key, layer, value);
|
|
3346
|
+
if (spec.type === "number")
|
|
3347
|
+
return decodeNumber(key, layer, value, spec);
|
|
3348
|
+
return decodeString(key, layer, value, spec);
|
|
3349
|
+
};
|
|
3350
|
+
|
|
3351
|
+
// ../../packages/options/src/resolve.ts
|
|
3352
|
+
var LAYERS = ["cli", "env", "file"];
|
|
3353
|
+
var resolveOptions = (specs, layers = {}) => {
|
|
3354
|
+
const values = {};
|
|
3355
|
+
for (const key of Object.keys(specs)) {
|
|
3356
|
+
const spec = specs[key];
|
|
3357
|
+
let found = false;
|
|
3358
|
+
for (const layer of LAYERS) {
|
|
3359
|
+
const source = layers[layer];
|
|
3360
|
+
if (source === undefined)
|
|
3361
|
+
continue;
|
|
3362
|
+
const raw = source[key];
|
|
3363
|
+
if (!isPresent(raw))
|
|
3364
|
+
continue;
|
|
3365
|
+
const decoded = decodeValue(key, spec, layer, raw);
|
|
3366
|
+
if (!decoded.ok)
|
|
3367
|
+
return decoded;
|
|
3368
|
+
values[key] = decoded.value;
|
|
3369
|
+
found = true;
|
|
3370
|
+
break;
|
|
3371
|
+
}
|
|
3372
|
+
if (!found)
|
|
3373
|
+
values[key] = spec.default;
|
|
3374
|
+
}
|
|
3375
|
+
return { ok: true, value: values };
|
|
3376
|
+
};
|
|
3377
|
+
var catalogDefaults = (specs) => {
|
|
3378
|
+
const values = {};
|
|
3379
|
+
for (const key of Object.keys(specs))
|
|
3380
|
+
values[key] = specs[key].default;
|
|
3381
|
+
return values;
|
|
3382
|
+
};
|
|
3383
|
+
|
|
3384
|
+
// ../../packages/options/src/session.ts
|
|
3385
|
+
var persistPolicy = (persist) => persist ?? "session";
|
|
3386
|
+
var createSession = (specs, values, options = {}) => {
|
|
3387
|
+
let snapshot = values;
|
|
3388
|
+
const listeners = new Set;
|
|
3389
|
+
const get = (key) => snapshot[key];
|
|
3390
|
+
const set = async (key, value) => {
|
|
3391
|
+
const spec = specs[key];
|
|
3392
|
+
if (spec === undefined) {
|
|
3393
|
+
throw new Error(`unknown option ${JSON.stringify(key)}`);
|
|
3394
|
+
}
|
|
3395
|
+
const decoded = decodeValue(key, spec, "cli", value);
|
|
3396
|
+
if (!decoded.ok) {
|
|
3397
|
+
throw new Error(`${decoded.error.key}: ${decoded.error.message}`);
|
|
3398
|
+
}
|
|
3399
|
+
if (Object.is(snapshot[key], decoded.value))
|
|
3400
|
+
return;
|
|
3401
|
+
snapshot = { ...snapshot, [key]: decoded.value };
|
|
3402
|
+
for (const listener of listeners)
|
|
3403
|
+
listener(snapshot);
|
|
3404
|
+
if (persistPolicy(spec.persist) === "file" && options.persist !== undefined) {
|
|
3405
|
+
await options.persist({
|
|
3406
|
+
key,
|
|
3407
|
+
value: decoded.value,
|
|
3408
|
+
values: snapshot
|
|
3409
|
+
});
|
|
3410
|
+
}
|
|
3411
|
+
};
|
|
3412
|
+
return {
|
|
3413
|
+
get,
|
|
3414
|
+
set,
|
|
3415
|
+
subscribe: (listener) => {
|
|
3416
|
+
listeners.add(listener);
|
|
3417
|
+
return () => {
|
|
3418
|
+
listeners.delete(listener);
|
|
3419
|
+
};
|
|
3420
|
+
},
|
|
3421
|
+
get values() {
|
|
3422
|
+
return snapshot;
|
|
3423
|
+
}
|
|
3424
|
+
};
|
|
3425
|
+
};
|
|
3426
|
+
|
|
3427
|
+
// ../../packages/options/src/define.ts
|
|
3428
|
+
var defineOptions = (specs) => {
|
|
3429
|
+
validateFooterSpecs(specs);
|
|
3430
|
+
return {
|
|
3431
|
+
specs,
|
|
3432
|
+
defaults: catalogDefaults(specs),
|
|
3433
|
+
resolve: (layers) => resolveOptions(specs, layers),
|
|
3434
|
+
createSession: (values, options) => createSession(specs, values, options)
|
|
3435
|
+
};
|
|
3436
|
+
};
|
|
3437
|
+
// src/Browser.tsx
|
|
3165
3438
|
import { useKeyboard, useRenderer, useTerminalDimensions as useTerminalDimensions2 } from "@opentui/react";
|
|
3166
3439
|
import { useAtomValue, useAtomSet } from "@effect/atom-react";
|
|
3167
3440
|
import { Effect as Effect3 } from "effect";
|
|
@@ -4090,7 +4363,7 @@ var BRAND = "\u2302";
|
|
|
4090
4363
|
var BRAND_NAME = "house";
|
|
4091
4364
|
|
|
4092
4365
|
// src/Footer.tsx
|
|
4093
|
-
import
|
|
4366
|
+
import stringWidth2 from "string-width";
|
|
4094
4367
|
|
|
4095
4368
|
// src/keymap/displayKey.ts
|
|
4096
4369
|
var displayKey = (raw) => {
|
|
@@ -4145,7 +4418,9 @@ var Spinner = ({
|
|
|
4145
4418
|
};
|
|
4146
4419
|
|
|
4147
4420
|
// src/StatusIndicator.tsx
|
|
4421
|
+
import stringWidth from "string-width";
|
|
4148
4422
|
import { jsxDEV as jsxDEV5 } from "@opentui/react/jsx-dev-runtime";
|
|
4423
|
+
var statusIndicatorWidth = (icon) => 2 + Math.max(1, stringWidth(icon));
|
|
4149
4424
|
var statusIndicatorFg = (variant) => {
|
|
4150
4425
|
switch (variant) {
|
|
4151
4426
|
case "info":
|
|
@@ -4164,18 +4439,20 @@ var StatusIndicator = ({
|
|
|
4164
4439
|
active = true,
|
|
4165
4440
|
onMouseUp
|
|
4166
4441
|
}) => {
|
|
4442
|
+
const glyph = icon.toLocaleUpperCase();
|
|
4167
4443
|
const activeColor = statusIndicatorFg(variant);
|
|
4168
4444
|
const backgroundColor = active ? activeColor : colors.backgroundElement;
|
|
4445
|
+
const width = statusIndicatorWidth(glyph);
|
|
4169
4446
|
return /* @__PURE__ */ jsxDEV5("box", {
|
|
4170
4447
|
...onMouseUp === undefined ? {} : { onMouseUp },
|
|
4171
4448
|
style: {
|
|
4172
|
-
width
|
|
4449
|
+
width,
|
|
4173
4450
|
height: 1,
|
|
4174
4451
|
flexDirection: "row",
|
|
4175
4452
|
backgroundColor
|
|
4176
4453
|
},
|
|
4177
4454
|
children: /* @__PURE__ */ jsxDEV5("text", {
|
|
4178
|
-
content: ` ${
|
|
4455
|
+
content: ` ${glyph} `,
|
|
4179
4456
|
wrapMode: "none",
|
|
4180
4457
|
style: {
|
|
4181
4458
|
fg: active ? colors.backgroundPanel : colors.textMuted,
|
|
@@ -4191,7 +4468,7 @@ import { jsxDEV as jsxDEV6 } from "@opentui/react/jsx-dev-runtime";
|
|
|
4191
4468
|
var FOOTER_HEIGHT = 1;
|
|
4192
4469
|
var normalizeStatusLine = (status) => status.replace(/\s+/g, " ").trim();
|
|
4193
4470
|
var HINT_SEPARATOR = " ";
|
|
4194
|
-
var hintWidth = (h) =>
|
|
4471
|
+
var hintWidth = (h) => stringWidth2(h.key) + 1 + stringWidth2(h.label);
|
|
4195
4472
|
var formatHint = (b) => {
|
|
4196
4473
|
if (!b.hint)
|
|
4197
4474
|
return null;
|
|
@@ -4218,6 +4495,8 @@ var fitHints = (hints, width) => {
|
|
|
4218
4495
|
return [{ key: first.key.slice(0, width), label: "" }];
|
|
4219
4496
|
};
|
|
4220
4497
|
var STATUS_SEPARATOR = " \xB7 ";
|
|
4498
|
+
var INDICATOR_SEPARATOR = " ";
|
|
4499
|
+
var INDICATOR_SEPARATOR_WIDTH = 1;
|
|
4221
4500
|
var NON_WARNING_STATUS_PREFIX_WIDTH = 2;
|
|
4222
4501
|
var isPartialDiscoveryWarning = (status) => status?.startsWith("scan incomplete:") ?? false;
|
|
4223
4502
|
var Footer = ({
|
|
@@ -4265,7 +4544,9 @@ var Footer = ({
|
|
|
4265
4544
|
] : [],
|
|
4266
4545
|
...indicators
|
|
4267
4546
|
];
|
|
4268
|
-
const
|
|
4547
|
+
const indicatorWidths = renderedIndicators.map((indicator) => statusIndicatorWidth(indicator.icon));
|
|
4548
|
+
const indicatorSeparatorBudget = renderedIndicators.length > 1 ? (renderedIndicators.length - 1) * INDICATOR_SEPARATOR_WIDTH : 0;
|
|
4549
|
+
const indicatorBudget = Math.min(usableWidth, indicatorWidths.reduce((sum, width2) => sum + width2, 0) + indicatorSeparatorBudget);
|
|
4269
4550
|
const hasContentAfterIndicators = notice !== null || status !== null || hints.length > 0;
|
|
4270
4551
|
const indicatorContentGapBudget = renderedIndicators.length > 0 && hasContentAfterIndicators && indicatorBudget < usableWidth ? 1 : 0;
|
|
4271
4552
|
const contentBudget = Math.max(0, usableWidth - indicatorBudget - indicatorContentGapBudget);
|
|
@@ -4277,9 +4558,21 @@ var Footer = ({
|
|
|
4277
4558
|
const statusContent = status ? status.slice(0, Math.max(0, statusBudget - statusChromeWidth)) : "";
|
|
4278
4559
|
const statusSeparatorContent = status ? STATUS_SEPARATOR.slice(0, Math.max(0, statusBudget - nonWarningStatusPrefixBudget - statusContent.length)) : "";
|
|
4279
4560
|
const noticeContent = notice ? notice.length > contentBudget ? notice.slice(0, contentBudget) : notice : null;
|
|
4280
|
-
const renderIndicators = () => renderedIndicators.
|
|
4281
|
-
|
|
4282
|
-
|
|
4561
|
+
const renderIndicators = () => renderedIndicators.flatMap(({ id, ...props }, index) => {
|
|
4562
|
+
const chip = /* @__PURE__ */ jsxDEV6(StatusIndicator, {
|
|
4563
|
+
...props
|
|
4564
|
+
}, id, false, undefined, this);
|
|
4565
|
+
if (index === 0)
|
|
4566
|
+
return [chip];
|
|
4567
|
+
return [
|
|
4568
|
+
/* @__PURE__ */ jsxDEV6("text", {
|
|
4569
|
+
content: INDICATOR_SEPARATOR,
|
|
4570
|
+
wrapMode: "none",
|
|
4571
|
+
style: { fg: colors.textMuted }
|
|
4572
|
+
}, `indicator-sep-${id}`, false, undefined, this),
|
|
4573
|
+
chip
|
|
4574
|
+
];
|
|
4575
|
+
});
|
|
4283
4576
|
const renderIndicatorContentGap = () => indicatorContentGapBudget > 0 ? /* @__PURE__ */ jsxDEV6("text", {
|
|
4284
4577
|
content: " ",
|
|
4285
4578
|
wrapMode: "none",
|
|
@@ -12742,192 +13035,72 @@ var map = loadBundledThemes();
|
|
|
12742
13035
|
var themeDefinitions = [...map.values()];
|
|
12743
13036
|
var getThemeDefinition = (id) => map.get(id);
|
|
12744
13037
|
|
|
12745
|
-
//
|
|
12746
|
-
var
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
|
-
|
|
12753
|
-
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
12757
|
-
|
|
12758
|
-
|
|
12759
|
-
|
|
12760
|
-
|
|
12761
|
-
|
|
12762
|
-
|
|
12763
|
-
|
|
12764
|
-
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
});
|
|
12769
|
-
var booleanMessage = "expected true or false";
|
|
12770
|
-
var numberMessage = (spec) => {
|
|
12771
|
-
if (spec.integer === true && spec.min === 1 && spec.max === undefined) {
|
|
12772
|
-
return "expected a positive integer";
|
|
12773
|
-
}
|
|
12774
|
-
if (spec.integer === true)
|
|
12775
|
-
return "expected an integer";
|
|
12776
|
-
return "expected a number";
|
|
12777
|
-
};
|
|
12778
|
-
var inRange = (value, spec) => {
|
|
12779
|
-
if (spec.min !== undefined && value < spec.min)
|
|
12780
|
-
return false;
|
|
12781
|
-
if (spec.max !== undefined && value > spec.max)
|
|
12782
|
-
return false;
|
|
12783
|
-
if (spec.integer === true && !Number.isSafeInteger(value))
|
|
12784
|
-
return false;
|
|
12785
|
-
if (!Number.isFinite(value))
|
|
12786
|
-
return false;
|
|
12787
|
-
return true;
|
|
12788
|
-
};
|
|
12789
|
-
var decodeBoolean = (key, layer, value) => {
|
|
12790
|
-
if (layer === "env") {
|
|
12791
|
-
if (value === "true")
|
|
12792
|
-
return { ok: true, value: true };
|
|
12793
|
-
if (value === "false")
|
|
12794
|
-
return { ok: true, value: false };
|
|
12795
|
-
return { ok: false, error: fail(key, layer, booleanMessage, value) };
|
|
12796
|
-
}
|
|
12797
|
-
if (typeof value === "boolean")
|
|
12798
|
-
return { ok: true, value };
|
|
12799
|
-
return { ok: false, error: fail(key, layer, booleanMessage, value) };
|
|
12800
|
-
};
|
|
12801
|
-
var decodeNumber = (key, layer, value, spec) => {
|
|
12802
|
-
const message = numberMessage(spec);
|
|
12803
|
-
if (layer === "env") {
|
|
12804
|
-
if (typeof value !== "string" || !/^-?\d+(\.\d+)?$/.test(value)) {
|
|
12805
|
-
return { ok: false, error: fail(key, layer, message, value) };
|
|
12806
|
-
}
|
|
12807
|
-
if (spec.integer === true && !/^-?\d+$/.test(value)) {
|
|
12808
|
-
return { ok: false, error: fail(key, layer, message, value) };
|
|
12809
|
-
}
|
|
12810
|
-
const parsed = spec.integer === true ? Number.parseInt(value, 10) : Number.parseFloat(value);
|
|
12811
|
-
if (!inRange(parsed, spec))
|
|
12812
|
-
return { ok: false, error: fail(key, layer, message, value) };
|
|
12813
|
-
return { ok: true, value: parsed };
|
|
12814
|
-
}
|
|
12815
|
-
if (typeof value !== "number" || !inRange(value, spec)) {
|
|
12816
|
-
return { ok: false, error: fail(key, layer, message, value) };
|
|
12817
|
-
}
|
|
12818
|
-
return { ok: true, value };
|
|
12819
|
-
};
|
|
12820
|
-
var decodeString = (key, layer, value, spec) => {
|
|
12821
|
-
if (typeof value !== "string") {
|
|
12822
|
-
const message = spec.choices === undefined ? "expected a string" : `expected one of ${spec.choices.join(", ")}`;
|
|
12823
|
-
return { ok: false, error: fail(key, layer, message, value) };
|
|
12824
|
-
}
|
|
12825
|
-
if (spec.choices !== undefined && !spec.choices.includes(value)) {
|
|
12826
|
-
return {
|
|
12827
|
-
ok: false,
|
|
12828
|
-
error: fail(key, layer, `expected one of ${spec.choices.join(", ")}`, value)
|
|
12829
|
-
};
|
|
13038
|
+
// src/config/footerControls.ts
|
|
13039
|
+
var footerControlsFromSession = (options, session, handlers = {}) => {
|
|
13040
|
+
const controls = [];
|
|
13041
|
+
for (const key of footerKeys(options.specs)) {
|
|
13042
|
+
const spec = options.specs[key];
|
|
13043
|
+
const footer = spec.footer;
|
|
13044
|
+
if (footer === undefined)
|
|
13045
|
+
continue;
|
|
13046
|
+
const value = session.get(key);
|
|
13047
|
+
const handler = handlers[key];
|
|
13048
|
+
controls.push({
|
|
13049
|
+
id: key,
|
|
13050
|
+
icon: footerControlGlyph(spec, value),
|
|
13051
|
+
variant: "info",
|
|
13052
|
+
active: handler?.active ?? footerControlActive(spec, value),
|
|
13053
|
+
onMouseUp: () => {
|
|
13054
|
+
if (handler?.onActivate !== undefined) {
|
|
13055
|
+
handler.onActivate();
|
|
13056
|
+
return;
|
|
13057
|
+
}
|
|
13058
|
+
const current = session.get(key);
|
|
13059
|
+
session.set(key, nextFooterValue(spec, current));
|
|
13060
|
+
}
|
|
13061
|
+
});
|
|
12830
13062
|
}
|
|
12831
|
-
return
|
|
12832
|
-
};
|
|
12833
|
-
var decodeValue = (key, spec, layer, value) => {
|
|
12834
|
-
if (spec.type === "boolean")
|
|
12835
|
-
return decodeBoolean(key, layer, value);
|
|
12836
|
-
if (spec.type === "number")
|
|
12837
|
-
return decodeNumber(key, layer, value, spec);
|
|
12838
|
-
return decodeString(key, layer, value, spec);
|
|
13063
|
+
return controls;
|
|
12839
13064
|
};
|
|
12840
13065
|
|
|
12841
|
-
//
|
|
12842
|
-
var
|
|
12843
|
-
|
|
12844
|
-
const
|
|
12845
|
-
for (const
|
|
12846
|
-
const
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
|
|
13066
|
+
// src/config/footerLabels.ts
|
|
13067
|
+
var uniqueFooterLabels = (ids) => {
|
|
13068
|
+
const used = new Set;
|
|
13069
|
+
const out = {};
|
|
13070
|
+
for (const id of ids) {
|
|
13071
|
+
const compact = id.replace(/[^a-zA-Z0-9]+/g, "").toLowerCase();
|
|
13072
|
+
const source = compact.length > 0 ? compact : "x";
|
|
13073
|
+
let label = source.slice(0, 2).padEnd(2, "x");
|
|
13074
|
+
let offset = 2;
|
|
13075
|
+
while (used.has(label)) {
|
|
13076
|
+
const next = source[offset];
|
|
13077
|
+
if (next !== undefined) {
|
|
13078
|
+
label = `${source[0]}${next}`;
|
|
13079
|
+
offset += 1;
|
|
13080
|
+
} else {
|
|
13081
|
+
label = `${source[0]}${used.size % 10}`;
|
|
13082
|
+
offset += 1;
|
|
13083
|
+
if (offset > source.length + 20) {
|
|
13084
|
+
throw new Error(`unable to allocate unique footer label for ${JSON.stringify(id)}`);
|
|
13085
|
+
}
|
|
13086
|
+
}
|
|
12861
13087
|
}
|
|
12862
|
-
|
|
12863
|
-
|
|
13088
|
+
used.add(label);
|
|
13089
|
+
out[id] = label;
|
|
12864
13090
|
}
|
|
12865
|
-
return
|
|
12866
|
-
};
|
|
12867
|
-
var catalogDefaults = (specs) => {
|
|
12868
|
-
const values = {};
|
|
12869
|
-
for (const key of Object.keys(specs))
|
|
12870
|
-
values[key] = specs[key].default;
|
|
12871
|
-
return values;
|
|
12872
|
-
};
|
|
12873
|
-
|
|
12874
|
-
// ../../packages/options/src/session.ts
|
|
12875
|
-
var persistPolicy = (persist) => persist ?? "session";
|
|
12876
|
-
var createSession = (specs, values, options = {}) => {
|
|
12877
|
-
let snapshot = values;
|
|
12878
|
-
const listeners = new Set;
|
|
12879
|
-
const get = (key) => snapshot[key];
|
|
12880
|
-
const set = async (key, value) => {
|
|
12881
|
-
const spec = specs[key];
|
|
12882
|
-
if (spec === undefined) {
|
|
12883
|
-
throw new Error(`unknown option ${JSON.stringify(key)}`);
|
|
12884
|
-
}
|
|
12885
|
-
const decoded = decodeValue(key, spec, "cli", value);
|
|
12886
|
-
if (!decoded.ok) {
|
|
12887
|
-
throw new Error(`${decoded.error.key}: ${decoded.error.message}`);
|
|
12888
|
-
}
|
|
12889
|
-
if (Object.is(snapshot[key], decoded.value))
|
|
12890
|
-
return;
|
|
12891
|
-
snapshot = { ...snapshot, [key]: decoded.value };
|
|
12892
|
-
for (const listener of listeners)
|
|
12893
|
-
listener(snapshot);
|
|
12894
|
-
if (persistPolicy(spec.persist) === "file" && options.persist !== undefined) {
|
|
12895
|
-
await options.persist({
|
|
12896
|
-
key,
|
|
12897
|
-
value: decoded.value,
|
|
12898
|
-
values: snapshot
|
|
12899
|
-
});
|
|
12900
|
-
}
|
|
12901
|
-
};
|
|
12902
|
-
return {
|
|
12903
|
-
get,
|
|
12904
|
-
set,
|
|
12905
|
-
subscribe: (listener) => {
|
|
12906
|
-
listeners.add(listener);
|
|
12907
|
-
return () => {
|
|
12908
|
-
listeners.delete(listener);
|
|
12909
|
-
};
|
|
12910
|
-
},
|
|
12911
|
-
get values() {
|
|
12912
|
-
return snapshot;
|
|
12913
|
-
}
|
|
12914
|
-
};
|
|
13091
|
+
return out;
|
|
12915
13092
|
};
|
|
12916
13093
|
|
|
12917
|
-
// ../../packages/options/src/define.ts
|
|
12918
|
-
var defineOptions = (specs) => ({
|
|
12919
|
-
specs,
|
|
12920
|
-
defaults: catalogDefaults(specs),
|
|
12921
|
-
resolve: (layers) => resolveOptions(specs, layers),
|
|
12922
|
-
createSession: (values, options) => createSession(specs, values, options)
|
|
12923
|
-
});
|
|
12924
13094
|
// src/config/options.ts
|
|
12925
13095
|
var FILE_NAVIGATOR_ORDERS = ["tree", "recently-modified"];
|
|
13096
|
+
var themeIds = themeDefinitions.map((theme) => theme.id);
|
|
13097
|
+
var themeFooterLabels = uniqueFooterLabels(themeIds);
|
|
12926
13098
|
var houseOptions = defineOptions({
|
|
12927
13099
|
wrap: {
|
|
12928
13100
|
type: "boolean",
|
|
12929
13101
|
default: false,
|
|
12930
|
-
persist: "session"
|
|
13102
|
+
persist: "session",
|
|
13103
|
+
footer: { icon: "W" }
|
|
12931
13104
|
},
|
|
12932
13105
|
width: {
|
|
12933
13106
|
type: "number",
|
|
@@ -12939,7 +13112,8 @@ var houseOptions = defineOptions({
|
|
|
12939
13112
|
type: "string",
|
|
12940
13113
|
default: "opencode",
|
|
12941
13114
|
persist: "file",
|
|
12942
|
-
choices:
|
|
13115
|
+
choices: themeIds,
|
|
13116
|
+
footer: { icon: "T", labels: themeFooterLabels }
|
|
12943
13117
|
},
|
|
12944
13118
|
tone: {
|
|
12945
13119
|
type: "string",
|
|
@@ -12960,7 +13134,15 @@ var houseOptions = defineOptions({
|
|
|
12960
13134
|
order: {
|
|
12961
13135
|
type: "string",
|
|
12962
13136
|
default: "recently-modified",
|
|
12963
|
-
choices: FILE_NAVIGATOR_ORDERS
|
|
13137
|
+
choices: FILE_NAVIGATOR_ORDERS,
|
|
13138
|
+
persist: "session",
|
|
13139
|
+
footer: {
|
|
13140
|
+
icon: "O",
|
|
13141
|
+
labels: {
|
|
13142
|
+
tree: "tr",
|
|
13143
|
+
"recently-modified": "re"
|
|
13144
|
+
}
|
|
13145
|
+
}
|
|
12964
13146
|
}
|
|
12965
13147
|
});
|
|
12966
13148
|
|
|
@@ -13297,6 +13479,7 @@ var persistHouseOption = async (event, path) => {
|
|
|
13297
13479
|
|
|
13298
13480
|
// src/Browser.tsx
|
|
13299
13481
|
import { jsxDEV as jsxDEV13 } from "@opentui/react/jsx-dev-runtime";
|
|
13482
|
+
var asFileNavigatorOrder = (value) => typeof value === "string" && FILE_NAVIGATOR_ORDERS.includes(value) ? value : "recently-modified";
|
|
13300
13483
|
var defaultReadFile = (path) => Effect3.runPromise(readFileText(path));
|
|
13301
13484
|
var defaultDiscoveryPolicy = {
|
|
13302
13485
|
revision: "house-markdown",
|
|
@@ -13378,15 +13561,17 @@ var Browser = ({
|
|
|
13378
13561
|
wrap: initialWrap,
|
|
13379
13562
|
width: wrapWidth,
|
|
13380
13563
|
theme: theme.id,
|
|
13381
|
-
tone: theme.tone
|
|
13564
|
+
tone: theme.tone,
|
|
13565
|
+
order: asFileNavigatorOrder(order)
|
|
13382
13566
|
}, { persist: persistHouseOption });
|
|
13383
13567
|
}
|
|
13384
13568
|
const wrapEnabled = useSyncExternalStore(optionsSession.current.subscribe, () => optionsSession.current.get("wrap"));
|
|
13569
|
+
const browseOrder = useSyncExternalStore(optionsSession.current.subscribe, () => asFileNavigatorOrder(optionsSession.current.get("order")));
|
|
13385
13570
|
const toggleWrap = () => {
|
|
13386
13571
|
const session = optionsSession.current;
|
|
13387
13572
|
if (session === null)
|
|
13388
13573
|
return;
|
|
13389
|
-
session.set("wrap",
|
|
13574
|
+
session.set("wrap", nextFooterValue(houseOptions.specs.wrap, session.get("wrap")));
|
|
13390
13575
|
};
|
|
13391
13576
|
const [loaded, setLoaded] = useState4(null);
|
|
13392
13577
|
const [error, setError] = useState4(null);
|
|
@@ -13606,6 +13791,14 @@ var Browser = ({
|
|
|
13606
13791
|
rememberAppearance(session.set("theme", next.id));
|
|
13607
13792
|
pushFooterNotice(`theme: ${next.name}`);
|
|
13608
13793
|
};
|
|
13794
|
+
const cycleOrder = () => {
|
|
13795
|
+
const session = optionsSession.current;
|
|
13796
|
+
if (session === null)
|
|
13797
|
+
return;
|
|
13798
|
+
const next = nextFooterValue(houseOptions.specs.order, session.get("order"));
|
|
13799
|
+
session.set("order", next);
|
|
13800
|
+
pushFooterNotice(`order: ${next}`);
|
|
13801
|
+
};
|
|
13609
13802
|
const toggleTone = () => {
|
|
13610
13803
|
const session = optionsSession.current;
|
|
13611
13804
|
if (session === null)
|
|
@@ -14326,15 +14519,11 @@ var Browser = ({
|
|
|
14326
14519
|
width,
|
|
14327
14520
|
notice: footerNotice?.text ?? null,
|
|
14328
14521
|
discoveryStatus: effectiveDiscoveryStatus,
|
|
14329
|
-
indicators: [
|
|
14330
|
-
{
|
|
14331
|
-
|
|
14332
|
-
|
|
14333
|
-
|
|
14334
|
-
active: wrapEnabled,
|
|
14335
|
-
onMouseUp: toggleWrap
|
|
14336
|
-
}
|
|
14337
|
-
],
|
|
14522
|
+
indicators: optionsSession.current === null ? [] : footerControlsFromSession(houseOptions, optionsSession.current, {
|
|
14523
|
+
wrap: { onActivate: toggleWrap },
|
|
14524
|
+
theme: { onActivate: () => cycleTheme(1) },
|
|
14525
|
+
order: { onActivate: cycleOrder }
|
|
14526
|
+
}),
|
|
14338
14527
|
...discoverySpinnerIntervalMs === undefined ? {} : { discoverySpinnerIntervalMs },
|
|
14339
14528
|
...discoverySpinnerInitialFrameIndex === undefined ? {} : { discoverySpinnerInitialFrameIndex },
|
|
14340
14529
|
...discoverySpinnerRegisterTick === undefined ? {} : { discoverySpinnerRegisterTick },
|
|
@@ -14369,7 +14558,7 @@ var Browser = ({
|
|
|
14369
14558
|
root,
|
|
14370
14559
|
policy,
|
|
14371
14560
|
watch: watch2,
|
|
14372
|
-
order,
|
|
14561
|
+
order: browseOrder,
|
|
14373
14562
|
debounceMs: filterDebounceMs,
|
|
14374
14563
|
navigatorRef,
|
|
14375
14564
|
snapshot: liveSnapshot,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carlesandres/house",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "TUI-first markdown reader on opentui",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
"version:set": "bun run dev/set-version.ts"
|
|
63
63
|
},
|
|
64
64
|
"optionalDependencies": {
|
|
65
|
-
"@carlesandres/house-darwin-arm64": "0.
|
|
66
|
-
"@carlesandres/house-darwin-x64": "0.
|
|
67
|
-
"@carlesandres/house-linux-arm64": "0.
|
|
68
|
-
"@carlesandres/house-linux-x64": "0.
|
|
65
|
+
"@carlesandres/house-darwin-arm64": "0.8.0",
|
|
66
|
+
"@carlesandres/house-darwin-x64": "0.8.0",
|
|
67
|
+
"@carlesandres/house-linux-arm64": "0.8.0",
|
|
68
|
+
"@carlesandres/house-linux-x64": "0.8.0"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"@effect/atom-react": "4.0.0-beta.60",
|