@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +32 -2
  2. package/README.md +10 -8
  3. package/dist/bin.js +92 -0
  4. package/dist/index.js +10005 -0
  5. package/package.json +15 -3
  6. package/src/Browser.tsx +0 -1210
  7. package/src/CommandPalette.tsx +0 -214
  8. package/src/Footer.tsx +0 -258
  9. package/src/Header.tsx +0 -71
  10. package/src/PromptRow.tsx +0 -51
  11. package/src/Spinner.tsx +0 -39
  12. package/src/StatusIndicator.tsx +0 -55
  13. package/src/StatusPopover.tsx +0 -166
  14. package/src/brand.ts +0 -7
  15. package/src/cli/argv.ts +0 -179
  16. package/src/commands/buildCommands.ts +0 -98
  17. package/src/commands/paletteOnlyCommands.ts +0 -27
  18. package/src/commands/score.ts +0 -78
  19. package/src/commands/types.ts +0 -26
  20. package/src/config/load.ts +0 -381
  21. package/src/config/save.ts +0 -95
  22. package/src/discovery/filter.ts +0 -148
  23. package/src/discovery/show.ts +0 -48
  24. package/src/discovery/walk.ts +0 -209
  25. package/src/index.tsx +0 -464
  26. package/src/io/clipboard.ts +0 -53
  27. package/src/io/editor.ts +0 -162
  28. package/src/io/readFile.ts +0 -14
  29. package/src/keymap/browser.ts +0 -335
  30. package/src/keymap/displayKey.ts +0 -17
  31. package/src/keymap/keymap.ts +0 -95
  32. package/src/layout/resolve.ts +0 -52
  33. package/src/layout/sidebarEmptyState.ts +0 -7
  34. package/src/layout/sidebarRow.ts +0 -69
  35. package/src/markdown/frontmatter.ts +0 -62
  36. package/src/serve/css.ts +0 -120
  37. package/src/serve/openBrowser.ts +0 -19
  38. package/src/serve/render.ts +0 -56
  39. package/src/serve/server.ts +0 -166
  40. package/src/theme/atom.ts +0 -23
  41. package/src/theme/colors.ts +0 -86
  42. package/src/theme/loader.ts +0 -110
  43. package/src/theme/registry.ts +0 -12
  44. package/src/theme/resolve.ts +0 -168
  45. package/src/theme/themes/aura.json +0 -58
  46. package/src/theme/themes/ayu.json +0 -69
  47. package/src/theme/themes/carbonfox.json +0 -201
  48. package/src/theme/themes/catppuccin-frappe.json +0 -186
  49. package/src/theme/themes/catppuccin-macchiato.json +0 -186
  50. package/src/theme/themes/catppuccin.json +0 -212
  51. package/src/theme/themes/cobalt2.json +0 -181
  52. package/src/theme/themes/cursor.json +0 -202
  53. package/src/theme/themes/dracula.json +0 -172
  54. package/src/theme/themes/everforest.json +0 -194
  55. package/src/theme/themes/flexoki.json +0 -190
  56. package/src/theme/themes/github.json +0 -186
  57. package/src/theme/themes/gruvbox.json +0 -195
  58. package/src/theme/themes/kanagawa.json +0 -180
  59. package/src/theme/themes/lucent-orng.json +0 -186
  60. package/src/theme/themes/material.json +0 -188
  61. package/src/theme/themes/matrix.json +0 -180
  62. package/src/theme/themes/mercury.json +0 -198
  63. package/src/theme/themes/monokai.json +0 -174
  64. package/src/theme/themes/nightowl.json +0 -174
  65. package/src/theme/themes/nord.json +0 -176
  66. package/src/theme/themes/one-dark.json +0 -184
  67. package/src/theme/themes/opencode.json +0 -198
  68. package/src/theme/themes/orng.json +0 -202
  69. package/src/theme/themes/osaka-jade.json +0 -193
  70. package/src/theme/themes/palenight.json +0 -175
  71. package/src/theme/themes/rosepine.json +0 -187
  72. package/src/theme/themes/solarized.json +0 -180
  73. package/src/theme/themes/synthwave84.json +0 -179
  74. package/src/theme/themes/tokyonight.json +0 -196
  75. package/src/theme/themes/vercel.json +0 -198
  76. package/src/theme/themes/vesper.json +0 -171
  77. package/src/theme/themes/zenburn.json +0 -176
  78. package/src/theme/types.ts +0 -115
  79. package/src/tips.ts +0 -88
  80. package/src/ui/middleTruncate.ts +0 -27
  81. package/src/update/cache.ts +0 -77
  82. package/src/update/check.ts +0 -165
  83. package/src/update/compare.ts +0 -41
  84. package/src/update/notice.ts +0 -29
  85. package/src/update/runtime.ts +0 -48
  86. package/src/update/useUpdateNotice.ts +0 -24
@@ -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
- : []
@@ -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
- }
@@ -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
- }