@carlesandres/house 0.4.6 → 0.4.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/CHANGELOG.md +57 -5
- package/README.md +23 -11
- package/package.json +1 -1
- package/src/Browser.tsx +321 -184
- package/src/CommandPalette.tsx +78 -14
- package/src/Footer.tsx +65 -31
- package/src/Header.tsx +4 -1
- package/src/Spinner.tsx +39 -0
- package/src/StatusPopover.tsx +189 -0
- package/src/cli/argv.ts +19 -6
- package/src/commands/buildCommands.ts +22 -33
- package/src/commands/score.ts +1 -1
- package/src/config/load.ts +23 -1
- package/src/discovery/walk.ts +20 -3
- package/src/index.tsx +159 -155
- package/src/keymap/browser.ts +17 -23
- package/src/keymap/keymap.ts +4 -4
- package/src/layout/sidebarEmptyState.ts +7 -0
- package/src/markdown/frontmatter.ts +62 -0
- package/src/serve/server.ts +1 -1
- package/src/theme/resolve.ts +1 -1
- package/src/theme/themes/aura.json +1 -1
- package/src/theme/themes/carbonfox.json +1 -1
- package/src/theme/themes/lucent-orng.json +4 -4
- package/src/theme/themes/nightowl.json +2 -2
- package/src/theme/themes/orng.json +2 -2
- package/src/theme/themes/solarized.json +6 -2
- package/src/theme/themes/vesper.json +2 -2
- package/src/tips.ts +0 -5
- package/src/update/check.ts +1 -1
- package/src/update/runtime.ts +1 -1
- package/src/HelpOverlay.tsx +0 -148
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Derive palette commands from `browserBindings` plus the annotation map.
|
|
3
3
|
*
|
|
4
4
|
* The annotation map is the *only* hand-written list keyed by binding id:
|
|
5
|
-
* it carries
|
|
6
|
-
*
|
|
7
|
-
* entry rather than a palette command. Every other binding is exposed
|
|
5
|
+
* it carries title rewrites and metadata for bindings whose raw
|
|
6
|
+
* `description` reads awkwardly as a palette command. Every enabled binding is exposed
|
|
8
7
|
* verbatim — its `description` becomes the palette `title`, its first key
|
|
9
8
|
* becomes the `shortcut`.
|
|
10
9
|
*
|
|
@@ -30,49 +29,40 @@ interface Annotation {
|
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* hidden in v1, reconsider if users ask. Title rewrites convert
|
|
37
|
-
* help-overlay phrasing ("Toggle sidebar visibility") into imperative
|
|
38
|
-
* palette phrasing ("Toggle sidebar"). See #70 design log §6.
|
|
32
|
+
* Title rewrites convert keymap phrasing into imperative palette phrasing.
|
|
33
|
+
* With the help overlay removed (#139), the palette is the only in-app full
|
|
34
|
+
* action index, so all currently-enabled actions stay discoverable here.
|
|
39
35
|
*/
|
|
40
36
|
const annotations: Record<string, Annotation> = {
|
|
41
37
|
// --- Keep, with title rewrites where the binding description reads awkwardly as a command ---
|
|
42
38
|
quit: { category: "App" },
|
|
43
39
|
"focus.toggle": { title: "Toggle focus", category: "View" },
|
|
44
40
|
"sidebar.toggle": { title: "Toggle sidebar", category: "View" },
|
|
45
|
-
"
|
|
41
|
+
"sidebar.down": { category: "Navigation" },
|
|
42
|
+
"sidebar.up": { category: "Navigation" },
|
|
43
|
+
"sidebar.jumpDown": { category: "Navigation" },
|
|
44
|
+
"sidebar.jumpUp": { category: "Navigation" },
|
|
45
|
+
"sidebar.pageDown": { category: "Navigation" },
|
|
46
|
+
"sidebar.pageUp": { category: "Navigation" },
|
|
47
|
+
"sidebar.top": { category: "Navigation" },
|
|
48
|
+
"sidebar.bottom": { category: "Navigation" },
|
|
49
|
+
"sidebar.open": { title: "Open file", category: "Navigation" },
|
|
46
50
|
"filter.open": { title: "Filter files…", category: "Navigation" },
|
|
51
|
+
"filter.clearOrOpen": { category: "Navigation" },
|
|
47
52
|
"discovery.toggleAll": {
|
|
48
53
|
title: "Toggle hidden / gitignored files",
|
|
49
54
|
category: "Navigation",
|
|
50
55
|
keywords: ["hidden", "gitignore", "dotfiles", "all"],
|
|
51
56
|
},
|
|
57
|
+
"reader.back": { title: "Back to sidebar", category: "Navigation" },
|
|
58
|
+
"reader.prevFile": { title: "Previous file", category: "Navigation" },
|
|
59
|
+
"reader.nextFile": { title: "Next file", category: "Navigation" },
|
|
52
60
|
"serve.current": { title: "Open in browser", category: "File" },
|
|
53
61
|
"file.edit": { title: "Open in editor", category: "File", keywords: ["editor", "vim", "vscode"] },
|
|
54
62
|
"theme.next": { category: "Appearance" },
|
|
55
63
|
"theme.prev": { category: "Appearance" },
|
|
56
64
|
"theme.toneToggle": { title: "Toggle dark/light tone", category: "Appearance" },
|
|
57
65
|
|
|
58
|
-
// --- Hide: pure keystroke navigation (j/k/space/b/g/G…) ---
|
|
59
|
-
"sidebar.down": { hidden: true },
|
|
60
|
-
"sidebar.up": { hidden: true },
|
|
61
|
-
"sidebar.jumpDown": { hidden: true },
|
|
62
|
-
"sidebar.jumpUp": { hidden: true },
|
|
63
|
-
"sidebar.pageDown": { hidden: true },
|
|
64
|
-
"sidebar.pageUp": { hidden: true },
|
|
65
|
-
"sidebar.top": { hidden: true },
|
|
66
|
-
"sidebar.bottom": { hidden: true },
|
|
67
|
-
|
|
68
|
-
// --- Hide: borderline reader nav. `[`/`]` and Return-to-open feel command-shaped
|
|
69
|
-
// but are pure keystroke navigation under the hood. #70 Q6a — reconsider
|
|
70
|
-
// if user feedback expects them in the palette.
|
|
71
|
-
"sidebar.open": { hidden: true },
|
|
72
|
-
"reader.back": { hidden: true },
|
|
73
|
-
"reader.prevFile": { hidden: true },
|
|
74
|
-
"reader.nextFile": { hidden: true },
|
|
75
|
-
|
|
76
66
|
// --- Hide: the palette opener itself shouldn't appear in the palette ---
|
|
77
67
|
"palette.open": { hidden: true },
|
|
78
68
|
}
|
|
@@ -80,8 +70,8 @@ const annotations: Record<string, Annotation> = {
|
|
|
80
70
|
/**
|
|
81
71
|
* Build the AppCommand list for a given render. Iterates `browserBindings`
|
|
82
72
|
* in array order (the empty-query palette renders in this order, by design
|
|
83
|
-
* — see #70 design log §empty-state ordering), drops hidden entries
|
|
84
|
-
* those whose `when` predicate currently returns false, and resolves
|
|
73
|
+
* — see #70 design log §empty-state ordering), drops explicitly-hidden entries
|
|
74
|
+
* and those whose `when` predicate currently returns false, and resolves
|
|
85
75
|
* annotations to populate title / category / keywords.
|
|
86
76
|
*/
|
|
87
77
|
export const buildCommands = (ctx: BrowserCtx): readonly AppCommand[] => {
|
|
@@ -89,9 +79,8 @@ export const buildCommands = (ctx: BrowserCtx): readonly AppCommand[] => {
|
|
|
89
79
|
for (const binding of browserBindings) {
|
|
90
80
|
const ann = annotations[binding.id]
|
|
91
81
|
if (ann?.hidden) continue
|
|
92
|
-
// Same gating the keymap dispatcher uses. Disabled bindings
|
|
93
|
-
//
|
|
94
|
-
// reason follow-up after the atom-driven migration).
|
|
82
|
+
// Same gating the keymap dispatcher uses. Disabled bindings stay out of
|
|
83
|
+
// the palette; #96 tracks a future show-with-reason mode.
|
|
95
84
|
if (binding.when && !binding.when(ctx)) continue
|
|
96
85
|
const cmd: AppCommand = {
|
|
97
86
|
id: binding.id,
|
package/src/commands/score.ts
CHANGED
|
@@ -34,7 +34,7 @@ const fuzzyIncludes = (text: string, query: string): boolean => {
|
|
|
34
34
|
|
|
35
35
|
const searchText = (command: AppCommand): string =>
|
|
36
36
|
normalize(
|
|
37
|
-
[command.title, command.category,
|
|
37
|
+
[command.title, command.category, ...(command.keywords ?? [])]
|
|
38
38
|
.filter((s): s is string => Boolean(s))
|
|
39
39
|
.join(" "),
|
|
40
40
|
)
|
package/src/config/load.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface HouseConfig {
|
|
|
20
20
|
readonly theme: string
|
|
21
21
|
readonly tone: "dark" | "light"
|
|
22
22
|
readonly mdx: boolean
|
|
23
|
+
/** Default discovery-root strategy when no explicit `--root` flag is passed. */
|
|
24
|
+
readonly defaultRoot: "cwd" | "git"
|
|
23
25
|
/** Categories of normally-skipped entries to opt into. See
|
|
24
26
|
* `src/discovery/show.ts` for the vocabulary. Empty array (the
|
|
25
27
|
* default) yields the conservative discovery set. */
|
|
@@ -43,6 +45,7 @@ export interface CliOverrides {
|
|
|
43
45
|
const DEFAULT_THEME = "opencode"
|
|
44
46
|
const DEFAULT_TONE: "dark" | "light" = "dark"
|
|
45
47
|
const DEFAULT_MDX = true
|
|
48
|
+
const DEFAULT_ROOT: "cwd" | "git" = "cwd"
|
|
46
49
|
const DEFAULT_SHOW = ""
|
|
47
50
|
const DEFAULT_FOCUS: "sidebar" | "reader" | "filter" = "filter"
|
|
48
51
|
|
|
@@ -54,11 +57,19 @@ const themeIds = themeDefinitions.map((t) => t.id)
|
|
|
54
57
|
* Used by `fileProvider` to warn about unrecognized keys (with a
|
|
55
58
|
* did-you-mean hint when one is close) while still loading the rest.
|
|
56
59
|
*/
|
|
57
|
-
const KNOWN_FILE_KEYS: ReadonlySet<string> = new Set([
|
|
60
|
+
const KNOWN_FILE_KEYS: ReadonlySet<string> = new Set([
|
|
61
|
+
"theme",
|
|
62
|
+
"tone",
|
|
63
|
+
"mdx",
|
|
64
|
+
"show",
|
|
65
|
+
"focus",
|
|
66
|
+
"defaultRoot",
|
|
67
|
+
])
|
|
58
68
|
|
|
59
69
|
const schema = Config.all({
|
|
60
70
|
theme: Config.schema(Schema.Literals(themeIds), "theme"),
|
|
61
71
|
tone: Config.schema(Schema.Literals(["dark", "light"] as const), "tone"),
|
|
72
|
+
defaultRoot: Config.schema(Schema.String, "defaultRoot"),
|
|
62
73
|
// Boolean stored as string literal because providers stringify values
|
|
63
74
|
// (TOML bools, env vars, CLI flags all flow through as text). Mapped to
|
|
64
75
|
// a real boolean in `loadConfig` below.
|
|
@@ -75,6 +86,7 @@ const defaultsProvider = (): ConfigProvider.ConfigProvider =>
|
|
|
75
86
|
ConfigProvider.fromUnknown({
|
|
76
87
|
theme: DEFAULT_THEME,
|
|
77
88
|
tone: DEFAULT_TONE,
|
|
89
|
+
defaultRoot: DEFAULT_ROOT,
|
|
78
90
|
mdx: String(DEFAULT_MDX),
|
|
79
91
|
show: DEFAULT_SHOW,
|
|
80
92
|
focus: DEFAULT_FOCUS,
|
|
@@ -192,11 +204,13 @@ const envProvider = (env: Record<string, string | undefined>): ConfigProvider.Co
|
|
|
192
204
|
const entries: Array<[string, string]> = []
|
|
193
205
|
const theme = env["HOUSE_THEME"]
|
|
194
206
|
const tone = env["HOUSE_TONE"]
|
|
207
|
+
const defaultRoot = env["HOUSE_DEFAULT_ROOT"]
|
|
195
208
|
const mdx = env["HOUSE_MDX"]
|
|
196
209
|
const show = env["HOUSE_SHOW"]
|
|
197
210
|
const focus = env["HOUSE_FOCUS"]
|
|
198
211
|
if (theme !== undefined) entries.push(["theme", theme])
|
|
199
212
|
if (tone !== undefined) entries.push(["tone", tone])
|
|
213
|
+
if (defaultRoot !== undefined) entries.push(["defaultRoot", defaultRoot])
|
|
200
214
|
if (mdx !== undefined) entries.push(["mdx", mdx])
|
|
201
215
|
if (show !== undefined) entries.push(["show", show])
|
|
202
216
|
if (focus !== undefined) entries.push(["focus", focus])
|
|
@@ -262,6 +276,13 @@ export const loadConfig = (
|
|
|
262
276
|
)
|
|
263
277
|
return schema.parse(provider).pipe(
|
|
264
278
|
Effect.flatMap((raw) => {
|
|
279
|
+
const defaultRoot =
|
|
280
|
+
raw.defaultRoot === "cwd" || raw.defaultRoot === "git" ? raw.defaultRoot : DEFAULT_ROOT
|
|
281
|
+
if (raw.defaultRoot !== defaultRoot) {
|
|
282
|
+
onWarning(
|
|
283
|
+
`house: ignoring invalid value ${JSON.stringify(raw.defaultRoot)} for defaultRoot in config/env; using "${DEFAULT_ROOT}"`,
|
|
284
|
+
)
|
|
285
|
+
}
|
|
265
286
|
const parsed = parseShowList(raw.show)
|
|
266
287
|
if (!parsed.ok) {
|
|
267
288
|
// Effect's `Config.ConfigError` requires a `SchemaError` or
|
|
@@ -278,6 +299,7 @@ export const loadConfig = (
|
|
|
278
299
|
return Effect.succeed({
|
|
279
300
|
theme: raw.theme,
|
|
280
301
|
tone: raw.tone,
|
|
302
|
+
defaultRoot,
|
|
281
303
|
mdx: raw.mdx === "true",
|
|
282
304
|
show: parsed.value,
|
|
283
305
|
focus: raw.focus,
|
package/src/discovery/walk.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface FileEntry {
|
|
|
13
13
|
readonly name: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export interface DiscoveryWarning {
|
|
17
|
+
readonly path: string
|
|
18
|
+
readonly cause: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
export type SortOrder = "dirs-first" | "files-first"
|
|
17
22
|
|
|
18
23
|
export interface WalkOptions {
|
|
@@ -25,6 +30,8 @@ export interface WalkOptions {
|
|
|
25
30
|
readonly sort?: SortOrder
|
|
26
31
|
/** Include `.mdx` files alongside `.md`/`.markdown`. Default `true`. */
|
|
27
32
|
readonly mdx?: boolean
|
|
33
|
+
/** Non-fatal subtree read errors. Root-level failures still error the walk. */
|
|
34
|
+
readonly onWarning?: ((warning: DiscoveryWarning) => void) | null
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
export class DiscoveryError extends Data.TaggedError("DiscoveryError")<{
|
|
@@ -93,9 +100,11 @@ async function* walkDirGen(
|
|
|
93
100
|
rootPath: string,
|
|
94
101
|
parentLevels: readonly IgnoreLevel[],
|
|
95
102
|
opts: { showHidden: boolean; showGitignored: boolean; sort: SortOrder; mdx: boolean },
|
|
103
|
+
onWarning: ((warning: DiscoveryWarning) => void) | null,
|
|
96
104
|
signal: AbortSignal,
|
|
97
105
|
): AsyncGenerator<FileEntry, void, void> {
|
|
98
106
|
if (signal.aborted) return
|
|
107
|
+
const isRoot = dirPath === rootPath
|
|
99
108
|
|
|
100
109
|
let levels = parentLevels
|
|
101
110
|
if (!opts.showGitignored) {
|
|
@@ -104,7 +113,14 @@ async function* walkDirGen(
|
|
|
104
113
|
if (ig) levels = [...parentLevels, { dir: dirPath, ig }]
|
|
105
114
|
}
|
|
106
115
|
|
|
107
|
-
|
|
116
|
+
let raw
|
|
117
|
+
try {
|
|
118
|
+
raw = await readdir(dirPath, { withFileTypes: true })
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (isRoot) throw error
|
|
121
|
+
onWarning?.({ path: dirPath, cause: error })
|
|
122
|
+
return
|
|
123
|
+
}
|
|
108
124
|
if (signal.aborted) return
|
|
109
125
|
|
|
110
126
|
for (const entry of sortEntries(raw, opts.sort)) {
|
|
@@ -120,7 +136,7 @@ async function* walkDirGen(
|
|
|
120
136
|
if (HARD_SKIP_DIRS.has(entry.name)) continue
|
|
121
137
|
if (!opts.showHidden && entry.name.startsWith(".")) continue
|
|
122
138
|
if (!opts.showGitignored && isIgnored(entryPath, true, levels)) continue
|
|
123
|
-
yield* walkDirGen(entryPath, rootPath, levels, opts, signal)
|
|
139
|
+
yield* walkDirGen(entryPath, rootPath, levels, opts, onWarning, signal)
|
|
124
140
|
continue
|
|
125
141
|
}
|
|
126
142
|
|
|
@@ -165,10 +181,11 @@ export const walk = (
|
|
|
165
181
|
sort: options.sort ?? ("dirs-first" as SortOrder),
|
|
166
182
|
mdx: options.mdx ?? true,
|
|
167
183
|
}
|
|
184
|
+
const onWarning = options.onWarning ?? null
|
|
168
185
|
const controller = new AbortController()
|
|
169
186
|
const iterable: AsyncIterable<FileEntry> = {
|
|
170
187
|
[Symbol.asyncIterator]() {
|
|
171
|
-
const gen = walkDirGen(absRoot, absRoot, [], opts, controller.signal)
|
|
188
|
+
const gen = walkDirGen(absRoot, absRoot, [], opts, onWarning, controller.signal)
|
|
172
189
|
return {
|
|
173
190
|
next: () => gen.next(),
|
|
174
191
|
return: async (value?: void) => {
|