@carlesandres/house 0.4.13 → 0.5.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 +32 -2
- package/README.md +10 -8
- package/dist/bin.js +92 -0
- package/dist/index.js +10005 -0
- package/package.json +15 -3
- package/src/Browser.tsx +0 -1210
- package/src/CommandPalette.tsx +0 -214
- package/src/Footer.tsx +0 -258
- package/src/Header.tsx +0 -71
- package/src/PromptRow.tsx +0 -51
- package/src/Spinner.tsx +0 -39
- package/src/StatusIndicator.tsx +0 -55
- package/src/StatusPopover.tsx +0 -166
- package/src/brand.ts +0 -7
- package/src/cli/argv.ts +0 -179
- package/src/commands/buildCommands.ts +0 -98
- package/src/commands/paletteOnlyCommands.ts +0 -27
- package/src/commands/score.ts +0 -78
- package/src/commands/types.ts +0 -26
- package/src/config/load.ts +0 -381
- package/src/config/save.ts +0 -95
- package/src/discovery/filter.ts +0 -148
- package/src/discovery/show.ts +0 -48
- package/src/discovery/walk.ts +0 -209
- package/src/index.tsx +0 -464
- package/src/io/clipboard.ts +0 -53
- package/src/io/editor.ts +0 -162
- package/src/io/readFile.ts +0 -14
- package/src/keymap/browser.ts +0 -335
- package/src/keymap/displayKey.ts +0 -17
- package/src/keymap/keymap.ts +0 -95
- package/src/layout/resolve.ts +0 -52
- package/src/layout/sidebarEmptyState.ts +0 -7
- package/src/layout/sidebarRow.ts +0 -69
- package/src/markdown/frontmatter.ts +0 -62
- package/src/serve/css.ts +0 -120
- package/src/serve/openBrowser.ts +0 -19
- package/src/serve/render.ts +0 -56
- package/src/serve/server.ts +0 -166
- package/src/theme/atom.ts +0 -23
- package/src/theme/colors.ts +0 -86
- package/src/theme/loader.ts +0 -110
- package/src/theme/registry.ts +0 -12
- package/src/theme/resolve.ts +0 -168
- package/src/theme/themes/aura.json +0 -58
- package/src/theme/themes/ayu.json +0 -69
- package/src/theme/themes/carbonfox.json +0 -201
- package/src/theme/themes/catppuccin-frappe.json +0 -186
- package/src/theme/themes/catppuccin-macchiato.json +0 -186
- package/src/theme/themes/catppuccin.json +0 -212
- package/src/theme/themes/cobalt2.json +0 -181
- package/src/theme/themes/cursor.json +0 -202
- package/src/theme/themes/dracula.json +0 -172
- package/src/theme/themes/everforest.json +0 -194
- package/src/theme/themes/flexoki.json +0 -190
- package/src/theme/themes/github.json +0 -186
- package/src/theme/themes/gruvbox.json +0 -195
- package/src/theme/themes/kanagawa.json +0 -180
- package/src/theme/themes/lucent-orng.json +0 -186
- package/src/theme/themes/material.json +0 -188
- package/src/theme/themes/matrix.json +0 -180
- package/src/theme/themes/mercury.json +0 -198
- package/src/theme/themes/monokai.json +0 -174
- package/src/theme/themes/nightowl.json +0 -174
- package/src/theme/themes/nord.json +0 -176
- package/src/theme/themes/one-dark.json +0 -184
- package/src/theme/themes/opencode.json +0 -198
- package/src/theme/themes/orng.json +0 -202
- package/src/theme/themes/osaka-jade.json +0 -193
- package/src/theme/themes/palenight.json +0 -175
- package/src/theme/themes/rosepine.json +0 -187
- package/src/theme/themes/solarized.json +0 -180
- package/src/theme/themes/synthwave84.json +0 -179
- package/src/theme/themes/tokyonight.json +0 -196
- package/src/theme/themes/vercel.json +0 -198
- package/src/theme/themes/vesper.json +0 -171
- package/src/theme/themes/zenburn.json +0 -176
- package/src/theme/types.ts +0 -115
- package/src/tips.ts +0 -88
- package/src/ui/middleTruncate.ts +0 -27
- package/src/update/cache.ts +0 -77
- package/src/update/check.ts +0 -165
- package/src/update/compare.ts +0 -41
- package/src/update/notice.ts +0 -29
- package/src/update/runtime.ts +0 -48
- package/src/update/useUpdateNotice.ts +0 -24
package/src/StatusPopover.tsx
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { useTerminalDimensions } from "@opentui/react"
|
|
2
|
-
import { useMemo, useState } from "react"
|
|
3
|
-
import { StatusIndicator, statusIndicatorFg } from "./StatusIndicator.tsx"
|
|
4
|
-
import { colors } from "./theme/colors.ts"
|
|
5
|
-
|
|
6
|
-
export type StatusPopoverVariant = "info" | "warning" | "error" | "success"
|
|
7
|
-
|
|
8
|
-
export interface StatusPopoverProps {
|
|
9
|
-
readonly icon: string
|
|
10
|
-
readonly content: string
|
|
11
|
-
readonly variant?: StatusPopoverVariant
|
|
12
|
-
readonly open?: boolean
|
|
13
|
-
readonly defaultOpen?: boolean
|
|
14
|
-
readonly onOpenChange?: (open: boolean) => void
|
|
15
|
-
readonly showPanel?: boolean
|
|
16
|
-
readonly minWidth?: number
|
|
17
|
-
readonly maxWidth?: number
|
|
18
|
-
readonly maxHeight?: number
|
|
19
|
-
readonly zIndex?: number
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface StatusPopoverPanelProps {
|
|
23
|
-
readonly content: string
|
|
24
|
-
readonly variant?: StatusPopoverVariant
|
|
25
|
-
readonly minWidth?: number
|
|
26
|
-
readonly maxWidth?: number
|
|
27
|
-
readonly maxHeight?: number
|
|
28
|
-
readonly zIndex?: number
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n))
|
|
32
|
-
|
|
33
|
-
const measureLine = (line: string): number => line.length
|
|
34
|
-
|
|
35
|
-
const wrapLine = (line: string, width: number): string[] => {
|
|
36
|
-
if (width <= 0) return [line]
|
|
37
|
-
if (line.length <= width) return [line]
|
|
38
|
-
const out: string[] = []
|
|
39
|
-
let i = 0
|
|
40
|
-
while (i < line.length) {
|
|
41
|
-
out.push(line.slice(i, i + width))
|
|
42
|
-
i += width
|
|
43
|
-
}
|
|
44
|
-
return out
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const StatusPopover = ({
|
|
48
|
-
icon,
|
|
49
|
-
content,
|
|
50
|
-
variant = "warning",
|
|
51
|
-
open,
|
|
52
|
-
defaultOpen = false,
|
|
53
|
-
onOpenChange,
|
|
54
|
-
showPanel = true,
|
|
55
|
-
minWidth = 14,
|
|
56
|
-
maxWidth = 40,
|
|
57
|
-
maxHeight = 12,
|
|
58
|
-
zIndex = 30,
|
|
59
|
-
}: StatusPopoverProps) => {
|
|
60
|
-
const { width: viewportWidth, height: viewportHeight } = useTerminalDimensions()
|
|
61
|
-
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen)
|
|
62
|
-
const isOpen = open ?? uncontrolledOpen
|
|
63
|
-
const setOpen = (next: boolean) => {
|
|
64
|
-
if (open === undefined) setUncontrolledOpen(next)
|
|
65
|
-
onOpenChange?.(next)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const lines = useMemo(() => content.split(/\r?\n/), [content])
|
|
69
|
-
const textWidth = useMemo(() => {
|
|
70
|
-
const widest = Math.max(1, ...lines.map(measureLine))
|
|
71
|
-
return clamp(widest, minWidth, Math.min(maxWidth, Math.max(1, viewportWidth - 4)))
|
|
72
|
-
}, [content, lines, maxWidth, minWidth, viewportWidth])
|
|
73
|
-
|
|
74
|
-
const wrapped = useMemo(
|
|
75
|
-
() => lines.flatMap((line) => wrapLine(line, textWidth)),
|
|
76
|
-
[lines, textWidth],
|
|
77
|
-
)
|
|
78
|
-
const bodyHeight = Math.min(maxHeight, Math.max(1, wrapped.length))
|
|
79
|
-
const popoverWidth = Math.min(textWidth + 2, Math.max(1, viewportWidth - 2))
|
|
80
|
-
const popoverHeight = Math.min(bodyHeight + 2, Math.max(3, viewportHeight - 2))
|
|
81
|
-
|
|
82
|
-
const left = 1
|
|
83
|
-
const top = Math.max(0, viewportHeight - popoverHeight - 2)
|
|
84
|
-
|
|
85
|
-
const linesToRender = wrapped.slice(0, Math.max(0, popoverHeight - 2))
|
|
86
|
-
while (linesToRender.length < popoverHeight - 2) linesToRender.push("")
|
|
87
|
-
const borderColor = statusIndicatorFg(variant)
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<>
|
|
91
|
-
<StatusIndicator icon={icon} variant={variant} active onMouseUp={() => setOpen(!isOpen)} />
|
|
92
|
-
{showPanel && isOpen && (
|
|
93
|
-
<box
|
|
94
|
-
position="absolute"
|
|
95
|
-
left={left}
|
|
96
|
-
top={top}
|
|
97
|
-
width={popoverWidth}
|
|
98
|
-
height={popoverHeight}
|
|
99
|
-
zIndex={zIndex}
|
|
100
|
-
style={{
|
|
101
|
-
border: true,
|
|
102
|
-
borderColor,
|
|
103
|
-
backgroundColor: colors.backgroundPanel,
|
|
104
|
-
flexDirection: "column",
|
|
105
|
-
}}
|
|
106
|
-
>
|
|
107
|
-
<text content="" />
|
|
108
|
-
{linesToRender.map((line, i) => (
|
|
109
|
-
<text key={i} content={line} wrapMode="none" style={{ fg: colors.text }} />
|
|
110
|
-
))}
|
|
111
|
-
<text content="" />
|
|
112
|
-
</box>
|
|
113
|
-
)}
|
|
114
|
-
</>
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export const StatusPopoverPanel = ({
|
|
119
|
-
content,
|
|
120
|
-
variant = "warning",
|
|
121
|
-
minWidth = 14,
|
|
122
|
-
maxWidth = 40,
|
|
123
|
-
maxHeight = 12,
|
|
124
|
-
zIndex = 30,
|
|
125
|
-
}: StatusPopoverPanelProps) => {
|
|
126
|
-
const { width: viewportWidth, height: viewportHeight } = useTerminalDimensions()
|
|
127
|
-
const lines = useMemo(() => content.split(/\r?\n/), [content])
|
|
128
|
-
const textWidth = useMemo(() => {
|
|
129
|
-
const widest = Math.max(1, ...lines.map(measureLine))
|
|
130
|
-
return clamp(widest, minWidth, Math.min(maxWidth, Math.max(1, viewportWidth - 4)))
|
|
131
|
-
}, [lines, maxWidth, minWidth, viewportWidth])
|
|
132
|
-
const wrapped = useMemo(
|
|
133
|
-
() => lines.flatMap((line) => wrapLine(line, textWidth)),
|
|
134
|
-
[lines, textWidth],
|
|
135
|
-
)
|
|
136
|
-
const bodyHeight = Math.min(maxHeight, Math.max(1, wrapped.length))
|
|
137
|
-
const popoverWidth = Math.min(textWidth + 2, Math.max(1, viewportWidth - 2))
|
|
138
|
-
const popoverHeight = Math.min(bodyHeight + 2, Math.max(3, viewportHeight - 2))
|
|
139
|
-
const linesToRender = wrapped.slice(0, Math.max(0, popoverHeight - 2))
|
|
140
|
-
while (linesToRender.length < popoverHeight - 2) linesToRender.push("")
|
|
141
|
-
|
|
142
|
-
const borderColor = statusIndicatorFg(variant)
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<box
|
|
146
|
-
position="absolute"
|
|
147
|
-
left={1}
|
|
148
|
-
top={Math.max(0, viewportHeight - popoverHeight - 2)}
|
|
149
|
-
width={popoverWidth}
|
|
150
|
-
height={popoverHeight}
|
|
151
|
-
zIndex={zIndex}
|
|
152
|
-
style={{
|
|
153
|
-
border: true,
|
|
154
|
-
borderColor,
|
|
155
|
-
backgroundColor: colors.backgroundPanel,
|
|
156
|
-
flexDirection: "column",
|
|
157
|
-
}}
|
|
158
|
-
>
|
|
159
|
-
<text content="" />
|
|
160
|
-
{linesToRender.map((line, i) => (
|
|
161
|
-
<text key={i} content={line} wrapMode="none" style={{ fg: colors.text }} />
|
|
162
|
-
))}
|
|
163
|
-
<text content="" />
|
|
164
|
-
</box>
|
|
165
|
-
)
|
|
166
|
-
}
|
package/src/brand.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Brand mark. U+2302 HOUSE is a single-cell glyph in every monospace font
|
|
3
|
-
* we care about; falls back to a tofu box on rare fonts that lack it.
|
|
4
|
-
* Single import point so future placements (#76) stay in sync.
|
|
5
|
-
*/
|
|
6
|
-
export const BRAND = "⌂"
|
|
7
|
-
export const BRAND_NAME = "house"
|
package/src/cli/argv.ts
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander"
|
|
2
|
-
import { themeDefinitions } from "../theme/registry.ts"
|
|
3
|
-
|
|
4
|
-
export interface ParsedArgs {
|
|
5
|
-
/** First positional argument, or null if none was given. */
|
|
6
|
-
readonly path: string | null
|
|
7
|
-
/** Value of `--root <dir>`, or null. */
|
|
8
|
-
readonly root: string | null
|
|
9
|
-
/** Value of `--theme <id>`, or null. Validated by the boot layer against the registry. */
|
|
10
|
-
readonly theme: string | null
|
|
11
|
-
/** Value of `--tone dark|light`, or null. Validated by the boot layer. */
|
|
12
|
-
readonly tone: string | null
|
|
13
|
-
/** Value of `--width <N>`, or null. Validated by the boot layer (must be a positive integer). */
|
|
14
|
-
readonly width: string | null
|
|
15
|
-
/** Startup reader wrap override. Null means config/env/default decides. */
|
|
16
|
-
readonly wrap: boolean | null
|
|
17
|
-
/** True when both `--wrap` and `--no-wrap` were passed. */
|
|
18
|
-
readonly wrapConflict: boolean
|
|
19
|
-
/** True when `--serve` was passed: serve the positional path as HTML, skip TUI. */
|
|
20
|
-
readonly serve: boolean
|
|
21
|
-
/** Value of `--port <N>`, or null. Validated by the boot layer. */
|
|
22
|
-
readonly port: string | null
|
|
23
|
-
/** True when `--help` was passed. */
|
|
24
|
-
readonly help: boolean
|
|
25
|
-
/** True when `--version` was passed. */
|
|
26
|
-
readonly version: boolean
|
|
27
|
-
/** True when `--config-path` was passed: print resolved config path and exit. */
|
|
28
|
-
readonly configPath: boolean
|
|
29
|
-
/** True when `--no-update-check` was passed: suppress the npm-registry
|
|
30
|
-
* probe and the "update available" notice. Mirrors the
|
|
31
|
-
* `NO_UPDATE_NOTIFIER` env var so opt-out is reachable without env state. */
|
|
32
|
-
readonly noUpdateCheck: boolean
|
|
33
|
-
/** Raw value of `--ext [list]`, or null if absent. Comma-separated list. */
|
|
34
|
-
readonly extensions: string | null
|
|
35
|
-
/** Value of `--focus <mode>` (`sidebar`, `reader`, `filter`), or null.
|
|
36
|
-
* Validated by the boot layer. */
|
|
37
|
-
readonly focus: string | null
|
|
38
|
-
/** Raw value of `--show <list>`, or null if the flag wasn't passed.
|
|
39
|
-
* Comma-separated list of category names; the boot layer validates
|
|
40
|
-
* tokens against the known vocabulary (see `discovery/show.ts`).
|
|
41
|
-
* `--show ""` is a meaningful value: clears the set. */
|
|
42
|
-
readonly show: string | null
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const createProgram = () =>
|
|
46
|
-
new Command()
|
|
47
|
-
.allowUnknownOption(true)
|
|
48
|
-
.allowExcessArguments(true)
|
|
49
|
-
.exitOverride()
|
|
50
|
-
.helpOption(false)
|
|
51
|
-
.option("--theme [id]")
|
|
52
|
-
.option("--tone [mode]")
|
|
53
|
-
.option("--width [N]")
|
|
54
|
-
.option("--wrap")
|
|
55
|
-
.option("--no-wrap")
|
|
56
|
-
.option("--serve")
|
|
57
|
-
.option("--port [N]")
|
|
58
|
-
.option("--config-path")
|
|
59
|
-
.option("--no-update-check")
|
|
60
|
-
.option("--ext [list]")
|
|
61
|
-
.option("--focus [mode]")
|
|
62
|
-
.option("--show [list]")
|
|
63
|
-
.option("--root [dir]")
|
|
64
|
-
.option("-h, --help")
|
|
65
|
-
.option("-v, --version")
|
|
66
|
-
.argument("[path]")
|
|
67
|
-
|
|
68
|
-
const VALUE_FLAGS: ReadonlySet<string> = new Set([
|
|
69
|
-
"--theme",
|
|
70
|
-
"--tone",
|
|
71
|
-
"--width",
|
|
72
|
-
"--port",
|
|
73
|
-
"--focus",
|
|
74
|
-
"--show",
|
|
75
|
-
"--root",
|
|
76
|
-
"--ext",
|
|
77
|
-
])
|
|
78
|
-
|
|
79
|
-
const REMOVED_VALUE_FLAGS: ReadonlySet<string> = new Set(["--sort", "--sidebar"])
|
|
80
|
-
|
|
81
|
-
const BOOLEAN_FLAGS: ReadonlySet<string> = new Set([
|
|
82
|
-
"--serve",
|
|
83
|
-
"--config-path",
|
|
84
|
-
"--no-update-check",
|
|
85
|
-
"--wrap",
|
|
86
|
-
"--no-wrap",
|
|
87
|
-
"--help",
|
|
88
|
-
"-h",
|
|
89
|
-
"--version",
|
|
90
|
-
"-v",
|
|
91
|
-
])
|
|
92
|
-
|
|
93
|
-
const findPathArg = (argv: readonly string[]): string | null => {
|
|
94
|
-
for (let i = 0; i < argv.length; i++) {
|
|
95
|
-
const arg = argv[i]!
|
|
96
|
-
if (VALUE_FLAGS.has(arg) || REMOVED_VALUE_FLAGS.has(arg)) {
|
|
97
|
-
const next = argv[i + 1]
|
|
98
|
-
if (next !== undefined && !next.startsWith("-")) i++
|
|
99
|
-
continue
|
|
100
|
-
}
|
|
101
|
-
if (BOOLEAN_FLAGS.has(arg)) continue
|
|
102
|
-
if (arg.startsWith("-")) continue
|
|
103
|
-
return arg
|
|
104
|
-
}
|
|
105
|
-
return null
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Minimal argv parser.
|
|
110
|
-
*
|
|
111
|
-
* Does not validate flag values — boot layers do, so error messages can
|
|
112
|
-
* reference domain knowledge (registered themes, valid integer ranges)
|
|
113
|
-
* without coupling the parser to it.
|
|
114
|
-
*/
|
|
115
|
-
export const parseArgv = (argv: readonly string[]): ParsedArgs => {
|
|
116
|
-
const program = createProgram()
|
|
117
|
-
program.parse([...argv], { from: "user" })
|
|
118
|
-
const opts = program.opts<Record<string, unknown>>()
|
|
119
|
-
const pathArg = findPathArg(argv)
|
|
120
|
-
const stringOrNull = (value: unknown): string | null => (typeof value === "string" ? value : null)
|
|
121
|
-
const hasWrap = argv.includes("--wrap")
|
|
122
|
-
const hasNoWrap = argv.includes("--no-wrap")
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
path: typeof pathArg === "string" ? pathArg : null,
|
|
126
|
-
root: stringOrNull(opts["root"]),
|
|
127
|
-
theme: stringOrNull(opts["theme"]),
|
|
128
|
-
tone: stringOrNull(opts["tone"]),
|
|
129
|
-
width: stringOrNull(opts["width"]),
|
|
130
|
-
wrap: hasWrap ? true : hasNoWrap ? false : null,
|
|
131
|
-
wrapConflict: hasWrap && hasNoWrap,
|
|
132
|
-
serve: opts["serve"] === true,
|
|
133
|
-
port: stringOrNull(opts["port"]),
|
|
134
|
-
help: opts["help"] === true,
|
|
135
|
-
version: opts["version"] === true,
|
|
136
|
-
configPath: opts["configPath"] === true,
|
|
137
|
-
noUpdateCheck: opts["noUpdateCheck"] === true,
|
|
138
|
-
extensions: stringOrNull(opts["ext"]),
|
|
139
|
-
show: stringOrNull(opts["show"]),
|
|
140
|
-
focus: stringOrNull(opts["focus"]),
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const themeList = themeDefinitions.map((t) => t.id).join(", ")
|
|
145
|
-
|
|
146
|
-
export const usage = `usage:
|
|
147
|
-
house [query] [options]
|
|
148
|
-
house --serve <path> [--port N]
|
|
149
|
-
|
|
150
|
-
query initial filter query; omit to browse the full discovery root
|
|
151
|
-
|
|
152
|
-
options:
|
|
153
|
-
--theme <id> color theme: ${themeList} (default: opencode)
|
|
154
|
-
--tone <mode> dark or light (default: dark)
|
|
155
|
-
--width <N> reader wrap width used when wrapping is enabled (default: 80)
|
|
156
|
-
--wrap start with reader wrapping enabled
|
|
157
|
-
--no-wrap start with reader wrapping disabled
|
|
158
|
-
--show <list> reveal normally-skipped entries; comma-separated subset of:
|
|
159
|
-
hidden, gitignored. Use --show "" to clear.
|
|
160
|
-
--root <dir> discovery root to walk (overrides defaultRoot config/env)
|
|
161
|
-
--focus <m> startup focus: sidebar, reader, or filter (default: sidebar)
|
|
162
|
-
--serve serve the positional path as HTML in the browser (skips TUI)
|
|
163
|
-
--port <N> port for --serve (default: OS-assigned)
|
|
164
|
-
-h, --help show this help and exit
|
|
165
|
-
-v, --version print version and exit
|
|
166
|
-
--config-path print path to the config file and exit
|
|
167
|
-
--no-update-check suppress the "newer version available" check (also via NO_UPDATE_NOTIFIER=1)
|
|
168
|
-
--ext <list> include extra file extensions (comma-separated)
|
|
169
|
-
|
|
170
|
-
examples:
|
|
171
|
-
house README.md
|
|
172
|
-
house --root docs
|
|
173
|
-
house --serve README.md
|
|
174
|
-
|
|
175
|
-
configuration:
|
|
176
|
-
file: $XDG_CONFIG_HOME/house/config.toml (default ~/.config/house/config.toml)
|
|
177
|
-
keys: theme, tone, width, wrap, extensions, show, focus, defaultRoot
|
|
178
|
-
env: HOUSE_THEME, HOUSE_TONE, HOUSE_WIDTH, HOUSE_WRAP, HOUSE_EXTENSIONS, HOUSE_SHOW, HOUSE_FOCUS, HOUSE_DEFAULT_ROOT
|
|
179
|
-
precedence (high → low): flags → env → file → defaults`
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Derive palette commands from `browserBindings` plus the annotation map.
|
|
3
|
-
*
|
|
4
|
-
* The annotation map is the *only* hand-written list keyed by binding id:
|
|
5
|
-
* it carries title rewrites and metadata for bindings whose raw
|
|
6
|
-
* `description` reads awkwardly as a palette command. Every enabled binding is exposed
|
|
7
|
-
* verbatim — its `description` becomes the palette `title`, its first key
|
|
8
|
-
* becomes the `shortcut`.
|
|
9
|
-
*
|
|
10
|
-
* Commands close over the per-render `BrowserCtx`, same shape the keymap
|
|
11
|
-
* dispatcher uses, so the palette and the keymap fire the same action via
|
|
12
|
-
* different surfaces. See #70 design log §list construction (option 3b)
|
|
13
|
-
* for why this beat (a) a fully hand-written palette list and (b) deriving
|
|
14
|
-
* everything via #92's atom-driven registry.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { browserBindings, type BrowserCtx } from "../keymap/browser.ts"
|
|
18
|
-
import { paletteOnlyCommands } from "./paletteOnlyCommands.ts"
|
|
19
|
-
import type { AppCommand } from "./types.ts"
|
|
20
|
-
|
|
21
|
-
interface Annotation {
|
|
22
|
-
/** Override the binding's `description` for palette display. */
|
|
23
|
-
readonly title?: string
|
|
24
|
-
/** Carried for #91's category headers; unused in v1's flat list. */
|
|
25
|
-
readonly category?: string
|
|
26
|
-
/** If true, the binding does not appear in the palette. */
|
|
27
|
-
readonly hidden?: boolean
|
|
28
|
-
readonly keywords?: readonly string[]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
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.
|
|
35
|
-
*/
|
|
36
|
-
const annotations: Record<string, Annotation> = {
|
|
37
|
-
// --- Keep, with title rewrites where the binding description reads awkwardly as a command ---
|
|
38
|
-
quit: { category: "App" },
|
|
39
|
-
"focus.toggle": { title: "Toggle focus", category: "View" },
|
|
40
|
-
"sidebar.toggle": { title: "Toggle sidebar", category: "View" },
|
|
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" },
|
|
50
|
-
"filter.open": { title: "Filter files…", category: "Navigation" },
|
|
51
|
-
"filter.clearOrOpen": { category: "Navigation" },
|
|
52
|
-
"discovery.toggleAll": {
|
|
53
|
-
title: "Toggle hidden / gitignored files",
|
|
54
|
-
category: "Navigation",
|
|
55
|
-
keywords: ["hidden", "gitignore", "dotfiles", "all"],
|
|
56
|
-
},
|
|
57
|
-
"reader.back": { title: "Back to sidebar", category: "Navigation" },
|
|
58
|
-
"reader.wrap.toggle": { title: "Toggle reader wrap", category: "View" },
|
|
59
|
-
"reader.prevFile": { title: "Previous file", category: "Navigation" },
|
|
60
|
-
"reader.nextFile": { title: "Next file", category: "Navigation" },
|
|
61
|
-
"serve.current": { title: "Open in browser", category: "File" },
|
|
62
|
-
"file.edit": { title: "Open in editor", category: "File", keywords: ["editor", "vim", "vscode"] },
|
|
63
|
-
"theme.next": { category: "Appearance" },
|
|
64
|
-
"theme.prev": { category: "Appearance" },
|
|
65
|
-
"theme.toneToggle": { title: "Toggle dark/light tone", category: "Appearance" },
|
|
66
|
-
|
|
67
|
-
// --- Hide: the palette opener itself shouldn't appear in the palette ---
|
|
68
|
-
"palette.open": { hidden: true },
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Build the AppCommand list for a given render. Iterates `browserBindings`
|
|
73
|
-
* in array order (the empty-query palette renders in this order, by design
|
|
74
|
-
* — see #70 design log §empty-state ordering), drops explicitly-hidden entries
|
|
75
|
-
* and those whose `when` predicate currently returns false, and resolves
|
|
76
|
-
* annotations to populate title / category / keywords.
|
|
77
|
-
*/
|
|
78
|
-
export const buildCommands = (ctx: BrowserCtx): readonly AppCommand[] => {
|
|
79
|
-
const out: AppCommand[] = []
|
|
80
|
-
for (const binding of browserBindings) {
|
|
81
|
-
const ann = annotations[binding.id]
|
|
82
|
-
if (ann?.hidden) continue
|
|
83
|
-
// Same gating the keymap dispatcher uses. Disabled bindings stay out of
|
|
84
|
-
// the palette; #96 tracks a future show-with-reason mode.
|
|
85
|
-
if (binding.when && !binding.when(ctx)) continue
|
|
86
|
-
const cmd: AppCommand = {
|
|
87
|
-
id: binding.id,
|
|
88
|
-
title: ann?.title ?? binding.description,
|
|
89
|
-
...(ann?.category !== undefined && { category: ann.category }),
|
|
90
|
-
...(ann?.keywords !== undefined && { keywords: ann.keywords }),
|
|
91
|
-
...(binding.keys[0] !== undefined && { shortcut: binding.keys[0] }),
|
|
92
|
-
run: () => binding.run(ctx),
|
|
93
|
-
}
|
|
94
|
-
out.push(cmd)
|
|
95
|
-
}
|
|
96
|
-
for (const cmd of paletteOnlyCommands(ctx)) out.push(cmd)
|
|
97
|
-
return out
|
|
98
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Commands that exist *only* in the palette — no `browserBindings` entry.
|
|
3
|
-
*
|
|
4
|
-
* Empty in v1 by design (#70 Q6c). This file exists as scaffolding so
|
|
5
|
-
* future palette-only commands (#93 reveal-in-OS, #94 copy-path, …) have
|
|
6
|
-
* an obvious home that's already wired into `buildCommands.ts`.
|
|
7
|
-
*
|
|
8
|
-
* Commands here follow the same shape as keymap-derived ones: they take
|
|
9
|
-
* the per-render `BrowserCtx` so they can read selection state, mutate
|
|
10
|
-
* focus, surface notices, etc.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { BrowserCtx } from "../keymap/browser.ts"
|
|
14
|
-
import type { AppCommand } from "./types.ts"
|
|
15
|
-
|
|
16
|
-
export const paletteOnlyCommands = (ctx: BrowserCtx): readonly AppCommand[] =>
|
|
17
|
-
ctx.hasSelected
|
|
18
|
-
? [
|
|
19
|
-
{
|
|
20
|
-
id: "file.copyContents",
|
|
21
|
-
title: "Copy file contents",
|
|
22
|
-
category: "File",
|
|
23
|
-
keywords: ["copy", "clipboard", "contents", "markdown"],
|
|
24
|
-
run: () => ctx.copyCurrentContents(),
|
|
25
|
-
},
|
|
26
|
-
]
|
|
27
|
-
: []
|
package/src/commands/score.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Command palette scorer — ported from ghui's pre-May `commands.ts`.
|
|
3
|
-
*
|
|
4
|
-
* Tiered ranking: title-prefix → text-prefix → title-includes →
|
|
5
|
-
* text-includes → acronym → fuzzy-includes. Lower score wins. Ties broken
|
|
6
|
-
* by `browserBindings` array order (the `index` field) so the empty-query
|
|
7
|
-
* palette renders in the keymap's natural order. See #70 design log for
|
|
8
|
-
* why we didn't reach for `fuzzysort`.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { AppCommand } from "./types.ts"
|
|
12
|
-
|
|
13
|
-
const normalize = (text: string): string =>
|
|
14
|
-
text
|
|
15
|
-
.toLowerCase()
|
|
16
|
-
.replace(/[^a-z0-9#]+/g, " ")
|
|
17
|
-
.trim()
|
|
18
|
-
|
|
19
|
-
const acronym = (text: string): string =>
|
|
20
|
-
normalize(text)
|
|
21
|
-
.split(" ")
|
|
22
|
-
.filter(Boolean)
|
|
23
|
-
.map((word) => word[0])
|
|
24
|
-
.join("")
|
|
25
|
-
|
|
26
|
-
const fuzzyIncludes = (text: string, query: string): boolean => {
|
|
27
|
-
let index = 0
|
|
28
|
-
for (const char of text) {
|
|
29
|
-
if (char === query[index]) index++
|
|
30
|
-
if (index >= query.length) return true
|
|
31
|
-
}
|
|
32
|
-
return query.length === 0
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const searchText = (command: AppCommand): string =>
|
|
36
|
-
normalize(
|
|
37
|
-
[command.title, command.category, ...(command.keywords ?? [])]
|
|
38
|
-
.filter((s): s is string => Boolean(s))
|
|
39
|
-
.join(" "),
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
/** Returns the tier (lower = better) or `null` if the query doesn't match. */
|
|
43
|
-
const score = (command: AppCommand, query: string): number | null => {
|
|
44
|
-
const q = normalize(query)
|
|
45
|
-
if (q.length === 0) return 0
|
|
46
|
-
const title = normalize(command.title)
|
|
47
|
-
const text = searchText(command)
|
|
48
|
-
if (title.startsWith(q)) return 0
|
|
49
|
-
if (text.startsWith(q)) return 1
|
|
50
|
-
if (title.includes(q)) return 2
|
|
51
|
-
if (text.includes(q)) return 3
|
|
52
|
-
if (acronym(command.title).startsWith(q)) return 4
|
|
53
|
-
if (fuzzyIncludes(text, q.replaceAll(" ", ""))) return 5
|
|
54
|
-
return null
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Filter and sort commands against a query. With an empty query, every
|
|
59
|
-
* command tiers to 0 and the original index breaks ties — so the result is
|
|
60
|
-
* the input list in its original order (which v1 uses as `browserBindings`
|
|
61
|
-
* order). See #70 design log §empty-state ordering.
|
|
62
|
-
*/
|
|
63
|
-
export const filterCommands = (
|
|
64
|
-
commands: readonly AppCommand[],
|
|
65
|
-
query: string,
|
|
66
|
-
): readonly AppCommand[] =>
|
|
67
|
-
commands
|
|
68
|
-
.flatMap((command, index) => {
|
|
69
|
-
const tier = score(command, query)
|
|
70
|
-
return tier === null ? [] : [{ command, index, tier }]
|
|
71
|
-
})
|
|
72
|
-
.sort((a, b) => a.tier - b.tier || a.index - b.index)
|
|
73
|
-
.map(({ command }) => command)
|
|
74
|
-
|
|
75
|
-
export const clampSelectedIndex = (index: number, commands: readonly AppCommand[]): number => {
|
|
76
|
-
if (commands.length === 0) return 0
|
|
77
|
-
return Math.max(0, Math.min(commands.length - 1, index))
|
|
78
|
-
}
|
package/src/commands/types.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Palette command shape.
|
|
3
|
-
*
|
|
4
|
-
* Commands are derived at render time from `browserBindings` via the
|
|
5
|
-
* annotation map (`buildCommands.ts`). The `run` field closes over the
|
|
6
|
-
* per-render `BrowserCtx` so dispatching a command is equivalent to firing
|
|
7
|
-
* the corresponding key binding — single source of truth for the action,
|
|
8
|
-
* two surfaces (keymap + palette) that can invoke it.
|
|
9
|
-
*
|
|
10
|
-
* See issue #70 (and its design-log comments) for the rejected alternatives
|
|
11
|
-
* — atom-driven registry (#92), fully hand-written list, fuzzysort dep, etc.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export interface AppCommand {
|
|
15
|
-
/** Stable id; matches the binding's id for keymap-derived commands. */
|
|
16
|
-
readonly id: string
|
|
17
|
-
/** Imperative-phrased label shown in the palette row. */
|
|
18
|
-
readonly title: string
|
|
19
|
-
/** Optional category for #91's grouped headers. Carried but unused in v1. */
|
|
20
|
-
readonly category?: string
|
|
21
|
-
/** Extra match terms (synonyms, alt phrasings). */
|
|
22
|
-
readonly keywords?: readonly string[]
|
|
23
|
-
/** Display-only shortcut hint (first key of the binding, if any). */
|
|
24
|
-
readonly shortcut?: string
|
|
25
|
-
readonly run: () => void
|
|
26
|
-
}
|