@carlesandres/house 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +75 -137
- package/README.md +37 -20
- package/package.json +14 -12
- package/src/Browser.tsx +331 -226
- package/src/CommandPalette.tsx +67 -46
- package/src/Footer.tsx +71 -27
- package/src/Header.tsx +68 -0
- package/src/HelpOverlay.tsx +59 -41
- package/src/PromptRow.tsx +49 -0
- package/src/brand.ts +7 -0
- package/src/cli/argv.ts +34 -3
- package/src/commands/buildCommands.ts +1 -0
- package/src/config/load.ts +84 -19
- package/src/discovery/walk.ts +8 -3
- package/src/index.tsx +64 -11
- package/src/io/editor.ts +162 -0
- package/src/keymap/browser.ts +79 -22
- package/src/keymap/keymap.ts +6 -0
- package/src/layout/resolve.ts +9 -9
- package/src/layout/sidebarRow.ts +85 -0
- package/src/serve/server.ts +4 -1
- package/src/theme/colors.ts +51 -15
- package/src/theme/types.ts +7 -0
- package/src/update/cache.ts +77 -0
- package/src/update/check.ts +165 -0
- package/src/update/compare.ts +41 -0
- package/src/update/notice.ts +29 -0
- package/src/update/runtime.ts +48 -0
- package/src/update/useUpdateNotice.ts +24 -0
package/src/CommandPalette.tsx
CHANGED
|
@@ -9,9 +9,17 @@
|
|
|
9
9
|
* no mouse, no recency. See #91/#93/#94/#95/#96 for the follow-ups.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { RGBA } from "@opentui/core"
|
|
12
13
|
import { colors } from "./theme/colors.ts"
|
|
14
|
+
import { PromptRow } from "./PromptRow.tsx"
|
|
13
15
|
import type { AppCommand } from "./commands/types.ts"
|
|
14
16
|
|
|
17
|
+
// Semi-transparent black scrim painted across the viewport behind the modal.
|
|
18
|
+
// Opentui composites it over the chrome underneath, so the rest of the UI
|
|
19
|
+
// reads as darkened while the modal stays fully opaque. Mirrors opencode's
|
|
20
|
+
// dialog backdrop (cli/cmd/tui/ui/dialog.tsx).
|
|
21
|
+
const SCRIM = RGBA.fromInts(0, 0, 0, 150)
|
|
22
|
+
|
|
15
23
|
export interface CommandPaletteProps {
|
|
16
24
|
readonly commands: readonly AppCommand[]
|
|
17
25
|
readonly query: string
|
|
@@ -73,54 +81,67 @@ export const CommandPalette = ({
|
|
|
73
81
|
return (
|
|
74
82
|
<box
|
|
75
83
|
position="absolute"
|
|
76
|
-
left={
|
|
77
|
-
top={
|
|
78
|
-
width={
|
|
79
|
-
height={
|
|
84
|
+
left={0}
|
|
85
|
+
top={0}
|
|
86
|
+
width={viewportWidth}
|
|
87
|
+
height={viewportHeight}
|
|
80
88
|
zIndex={20}
|
|
81
|
-
|
|
82
|
-
titleAlignment="left"
|
|
83
|
-
paddingLeft={1}
|
|
84
|
-
paddingRight={1}
|
|
85
|
-
style={{
|
|
86
|
-
border: true,
|
|
87
|
-
borderColor: colors.borderActive,
|
|
88
|
-
flexDirection: "column",
|
|
89
|
-
backgroundColor: colors.surface,
|
|
90
|
-
}}
|
|
89
|
+
style={{ backgroundColor: SCRIM }}
|
|
91
90
|
>
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
91
|
+
<box
|
|
92
|
+
position="absolute"
|
|
93
|
+
left={left}
|
|
94
|
+
top={top}
|
|
95
|
+
width={overlayWidth}
|
|
96
|
+
height={overlayHeight}
|
|
97
|
+
title=" Commands "
|
|
98
|
+
titleAlignment="left"
|
|
99
|
+
paddingLeft={1}
|
|
100
|
+
paddingRight={1}
|
|
101
|
+
style={{
|
|
102
|
+
border: true,
|
|
103
|
+
borderColor: colors.textMuted,
|
|
104
|
+
flexDirection: "column",
|
|
105
|
+
backgroundColor: colors.surface,
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<PromptRow query={query} editing={true} width={rowWidth} />
|
|
109
|
+
<text content=" " />
|
|
110
|
+
{commands.length === 0 ? (
|
|
111
|
+
<text wrapMode="none" content=" (no matches)" style={{ fg: colors.textMuted }} />
|
|
112
|
+
) : (
|
|
113
|
+
visible.map((cmd, i) => {
|
|
114
|
+
const realIdx = scrollTop + i
|
|
115
|
+
const isSelected = realIdx === selectedIndex
|
|
116
|
+
const selector = isSelected ? "▸ " : " "
|
|
117
|
+
const titleText = fit(cmd.title, titleWidth)
|
|
118
|
+
const shortcutText = cmd.shortcut
|
|
119
|
+
? fitRight(cmd.shortcut, SHORTCUT_WIDTH)
|
|
120
|
+
: " ".repeat(SHORTCUT_WIDTH)
|
|
121
|
+
// Title and shortcut render as separate spans so the shortcut
|
|
122
|
+
// can use `textMuted` while the title uses `text`/`textStrong`.
|
|
123
|
+
// Same trick opencode pulls with `--text-weak` — the theme
|
|
124
|
+
// guarantees the contrast, we just pick the right role.
|
|
125
|
+
const titleFg = isSelected ? colors.textStrong : colors.text
|
|
126
|
+
return (
|
|
127
|
+
<text
|
|
128
|
+
key={cmd.id}
|
|
129
|
+
wrapMode="none"
|
|
130
|
+
style={isSelected ? { bg: colors.selectedBg } : {}}
|
|
131
|
+
>
|
|
132
|
+
<span style={{ fg: titleFg }}>{`${selector}${titleText} `}</span>
|
|
133
|
+
<span style={{ fg: colors.textMuted }}>{shortcutText}</span>
|
|
134
|
+
</text>
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
)}
|
|
138
|
+
<text content=" " />
|
|
139
|
+
<text
|
|
140
|
+
wrapMode="none"
|
|
141
|
+
content={fit(FOOTER_HINT, rowWidth)}
|
|
142
|
+
style={{ fg: colors.textMuted }}
|
|
143
|
+
/>
|
|
144
|
+
</box>
|
|
124
145
|
</box>
|
|
125
146
|
)
|
|
126
147
|
}
|
package/src/Footer.tsx
CHANGED
|
@@ -71,30 +71,40 @@ const displayKey = (raw: string): string => {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
/** Hint row entries. `key === null` is a standalone chip (e.g. the filter
|
|
75
|
+
* chip) and renders as muted text without the key/label split. */
|
|
76
|
+
interface Hint {
|
|
77
|
+
readonly key: string | null
|
|
78
|
+
readonly label: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const hintWidth = (h: Hint): number =>
|
|
82
|
+
h.key === null ? h.label.length : h.key.length + 1 + h.label.length // key + " " + label
|
|
83
|
+
|
|
84
|
+
const formatHint = <C,>(b: KeyBinding<C>): Hint | null => {
|
|
75
85
|
if (!b.hint) return null
|
|
76
86
|
const first = b.keys[0]
|
|
77
87
|
if (!first) return null
|
|
78
|
-
return
|
|
88
|
+
return { key: displayKey(first), label: b.hint }
|
|
79
89
|
}
|
|
80
90
|
|
|
81
|
-
/** Drop hints from the end until
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let
|
|
91
|
+
/** Drop hints from the end until they fit within `width`. If not even the
|
|
92
|
+
* first hint fits, fall back to the bare key (or label chip) truncated to
|
|
93
|
+
* width, so the row is never silently blank on tight viewports. */
|
|
94
|
+
const fitHints = (hints: readonly Hint[], width: number): Hint[] => {
|
|
95
|
+
if (width <= 0 || hints.length === 0) return []
|
|
96
|
+
const acc: Hint[] = []
|
|
97
|
+
let used = 0
|
|
88
98
|
for (const h of hints) {
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
acc
|
|
99
|
+
const add = acc.length === 0 ? hintWidth(h) : HINT_SEPARATOR.length + hintWidth(h)
|
|
100
|
+
if (used + add > width) break
|
|
101
|
+
acc.push(h)
|
|
102
|
+
used += add
|
|
92
103
|
}
|
|
93
104
|
if (acc.length > 0) return acc
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return firstKey.slice(0, width)
|
|
105
|
+
const first = hints[0]!
|
|
106
|
+
if (first.key === null) return [{ key: null, label: first.label.slice(0, width) }]
|
|
107
|
+
return [{ key: first.key.slice(0, width), label: "" }]
|
|
98
108
|
}
|
|
99
109
|
|
|
100
110
|
const STATUS_SEPARATOR = " · "
|
|
@@ -116,19 +126,23 @@ export const Footer = <C,>({
|
|
|
116
126
|
flexDirection: "row",
|
|
117
127
|
paddingLeft: 1,
|
|
118
128
|
paddingRight: 1,
|
|
119
|
-
backgroundColor: colors.
|
|
129
|
+
backgroundColor: colors.surface,
|
|
120
130
|
} as const
|
|
121
131
|
|
|
122
|
-
const hints:
|
|
132
|
+
const hints: Hint[] = []
|
|
123
133
|
// The filter chip prepends to the hint row when a filter is applied and the
|
|
124
134
|
// input is closed. Bracketed to avoid looking like a `key:hint` binding —
|
|
125
135
|
// "filter" is not a key. Surfaces the otherwise-invisible invariant that
|
|
126
136
|
// `[`/`]` walks the filtered set. See DESIGN.md §7.1 Q1.
|
|
127
137
|
if (filterQuery && filterQuery.length > 0) {
|
|
128
|
-
hints.push(`[filter: ${filterQuery}]`)
|
|
138
|
+
hints.push({ key: null, label: `[filter: ${filterQuery}]` })
|
|
129
139
|
}
|
|
130
140
|
for (const b of bindings) {
|
|
131
|
-
|
|
141
|
+
// Hint visibility prefers `hintWhen` (binding-specific) over `when`
|
|
142
|
+
// (dispatch gate). Falling back to `when` keeps the original
|
|
143
|
+
// "hint shows when binding is enabled" behavior for the common case.
|
|
144
|
+
const visibleGate = b.hintWhen ?? b.when
|
|
145
|
+
if (visibleGate && !visibleGate(ctx)) continue
|
|
132
146
|
const h = formatHint(b)
|
|
133
147
|
if (h !== null) hints.push(h)
|
|
134
148
|
}
|
|
@@ -139,7 +153,7 @@ export const Footer = <C,>({
|
|
|
139
153
|
const status = discoveryStatus && discoveryStatus.length > 0 ? discoveryStatus : null
|
|
140
154
|
const statusBudget = status ? Math.min(status.length + STATUS_SEPARATOR.length, usableWidth) : 0
|
|
141
155
|
const hintsWidth = Math.max(0, usableWidth - statusBudget)
|
|
142
|
-
const
|
|
156
|
+
const visibleHints = fitHints(hints, hintsWidth)
|
|
143
157
|
const statusContent = status
|
|
144
158
|
? status.slice(0, Math.max(0, statusBudget - STATUS_SEPARATOR.length))
|
|
145
159
|
: ""
|
|
@@ -150,6 +164,40 @@ export const Footer = <C,>({
|
|
|
150
164
|
: notice
|
|
151
165
|
: null
|
|
152
166
|
|
|
167
|
+
// Two-tone hint row: keys render in `text` (foreground-strength), the
|
|
168
|
+
// `:label` portion in `textMuted`. Matches ghui's footer treatment so
|
|
169
|
+
// the key — the actionable token — visually leads each hint.
|
|
170
|
+
const renderHints = () =>
|
|
171
|
+
visibleHints.flatMap((h, i) => {
|
|
172
|
+
const sep =
|
|
173
|
+
i > 0
|
|
174
|
+
? [
|
|
175
|
+
<text
|
|
176
|
+
key={`s${i}`}
|
|
177
|
+
content={HINT_SEPARATOR}
|
|
178
|
+
wrapMode="none"
|
|
179
|
+
style={{ fg: colors.textMuted }}
|
|
180
|
+
/>,
|
|
181
|
+
]
|
|
182
|
+
: []
|
|
183
|
+
if (h.key === null) {
|
|
184
|
+
return [
|
|
185
|
+
...sep,
|
|
186
|
+
<text key={`l${i}`} content={h.label} wrapMode="none" style={{ fg: colors.textMuted }} />,
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
return [
|
|
190
|
+
...sep,
|
|
191
|
+
<text key={`k${i}`} content={h.key} wrapMode="none" style={{ fg: colors.text }} />,
|
|
192
|
+
<text
|
|
193
|
+
key={`l${i}`}
|
|
194
|
+
content={` ${h.label}`}
|
|
195
|
+
wrapMode="none"
|
|
196
|
+
style={{ fg: colors.textMuted }}
|
|
197
|
+
/>,
|
|
198
|
+
]
|
|
199
|
+
})
|
|
200
|
+
|
|
153
201
|
// Priority: notice > (status + hints). Notice fg is strong; status sits
|
|
154
202
|
// at the muted level so it reads as ambient state, not an event.
|
|
155
203
|
if (noticeContent !== null) {
|
|
@@ -165,14 +213,10 @@ export const Footer = <C,>({
|
|
|
165
213
|
<box style={rowStyle}>
|
|
166
214
|
<text content={statusContent} wrapMode="none" style={{ fg: colors.textMuted }} />
|
|
167
215
|
<text content={STATUS_SEPARATOR} wrapMode="none" style={{ fg: colors.textMuted }} />
|
|
168
|
-
|
|
216
|
+
{renderHints()}
|
|
169
217
|
</box>
|
|
170
218
|
)
|
|
171
219
|
}
|
|
172
220
|
|
|
173
|
-
return (
|
|
174
|
-
<box style={rowStyle}>
|
|
175
|
-
<text content={hintContent} wrapMode="none" style={{ fg: colors.textMuted }} />
|
|
176
|
-
</box>
|
|
177
|
-
)
|
|
221
|
+
return <box style={rowStyle}>{renderHints()}</box>
|
|
178
222
|
}
|
package/src/Header.tsx
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header — single-row chrome above the two-pane area.
|
|
3
|
+
*
|
|
4
|
+
* Borderless single line modeled on ghui's PlainLine header: brand and
|
|
5
|
+
* current filename on the left, version on the right. The row is
|
|
6
|
+
* informational, not interactive — see issue #38 for the design discussion.
|
|
7
|
+
*
|
|
8
|
+
* Width degrades gracefully: the version drops first when the row gets
|
|
9
|
+
* tight, then the filename, leaving the brand mark as the irreducible
|
|
10
|
+
* identity element. Always rendered — the row is worth one cell on any
|
|
11
|
+
* viewport so the user never loses the filename indicator (notably, when
|
|
12
|
+
* the sidebar drawer overlays the reader on narrow viewports).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import pkg from "../package.json" with { type: "json" }
|
|
16
|
+
import { BRAND, BRAND_NAME } from "./brand.ts"
|
|
17
|
+
import { colors } from "./theme/colors.ts"
|
|
18
|
+
|
|
19
|
+
export const HEADER_HEIGHT = 1
|
|
20
|
+
|
|
21
|
+
const FILE_SEPARATOR = " · "
|
|
22
|
+
|
|
23
|
+
export interface HeaderProps {
|
|
24
|
+
readonly width: number
|
|
25
|
+
/** Currently selected file's relative path. When set, the Header shows
|
|
26
|
+
* it next to the brand mark — replaces the per-pane border title that
|
|
27
|
+
* used to carry this information. */
|
|
28
|
+
readonly currentFile?: string | null
|
|
29
|
+
/** Optional override for the version string (testing). Defaults to
|
|
30
|
+
* the running package's version. */
|
|
31
|
+
readonly version?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const Header = ({ width, currentFile, version = pkg.version }: HeaderProps) => {
|
|
35
|
+
const brand = `${BRAND} ${BRAND_NAME}`
|
|
36
|
+
const right = `v${version}`
|
|
37
|
+
const file = currentFile && currentFile.length > 0 ? currentFile : null
|
|
38
|
+
const usableWidth = Math.max(0, width - 2) // 1-cell horizontal padding each side
|
|
39
|
+
|
|
40
|
+
// Priority: brand > filename > version. Brand is the irreducible identity
|
|
41
|
+
// anchor. Filename is per-selection useful info — keep it before the
|
|
42
|
+
// largely-static version string. `1` is the minimum gap between left and
|
|
43
|
+
// right groups so they never visually collide.
|
|
44
|
+
const leftWithFile = file !== null ? `${brand}${FILE_SEPARATOR}${file}` : brand
|
|
45
|
+
const showFileWithVersion = leftWithFile.length + 1 + right.length <= usableWidth
|
|
46
|
+
const showFileWithoutVersion = leftWithFile.length <= usableWidth
|
|
47
|
+
const showFile = file !== null && (showFileWithVersion || showFileWithoutVersion)
|
|
48
|
+
const left = showFile ? leftWithFile : brand
|
|
49
|
+
const showRight = left.length + 1 + right.length <= usableWidth
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<box
|
|
53
|
+
style={{
|
|
54
|
+
width,
|
|
55
|
+
height: HEADER_HEIGHT,
|
|
56
|
+
flexShrink: 0,
|
|
57
|
+
flexDirection: "row",
|
|
58
|
+
justifyContent: "space-between",
|
|
59
|
+
paddingLeft: 1,
|
|
60
|
+
paddingRight: 1,
|
|
61
|
+
backgroundColor: colors.surface,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<text content={left} wrapMode="none" style={{ fg: colors.text }} />
|
|
65
|
+
{showRight && <text content={right} wrapMode="none" style={{ fg: colors.text }} />}
|
|
66
|
+
</box>
|
|
67
|
+
)
|
|
68
|
+
}
|
package/src/HelpOverlay.tsx
CHANGED
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
* keys — the dispatcher and the help text are the same source of truth.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { RGBA } from "@opentui/core"
|
|
9
10
|
import type { KeyBinding } from "./keymap/keymap.ts"
|
|
10
11
|
import { colors } from "./theme/colors.ts"
|
|
11
12
|
|
|
13
|
+
// See CommandPalette.tsx for the scrim rationale.
|
|
14
|
+
const SCRIM = RGBA.fromInts(0, 0, 0, 150)
|
|
15
|
+
|
|
12
16
|
export interface HelpOverlayProps<C> {
|
|
13
17
|
readonly bindings: readonly KeyBinding<C>[]
|
|
14
18
|
readonly viewportWidth: number
|
|
@@ -82,49 +86,63 @@ export const HelpOverlay = <C,>({
|
|
|
82
86
|
return (
|
|
83
87
|
<box
|
|
84
88
|
position="absolute"
|
|
85
|
-
left={
|
|
86
|
-
top={
|
|
87
|
-
width={
|
|
88
|
-
height={
|
|
89
|
+
left={0}
|
|
90
|
+
top={0}
|
|
91
|
+
width={viewportWidth}
|
|
92
|
+
height={viewportHeight}
|
|
89
93
|
zIndex={10}
|
|
90
|
-
|
|
91
|
-
titleAlignment="left"
|
|
92
|
-
style={{
|
|
93
|
-
border: true,
|
|
94
|
-
borderColor: colors.borderActive,
|
|
95
|
-
padding: 1,
|
|
96
|
-
flexDirection: "column",
|
|
97
|
-
backgroundColor: colors.surface,
|
|
98
|
-
}}
|
|
94
|
+
style={{ backgroundColor: SCRIM }}
|
|
99
95
|
>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
96
|
+
<box
|
|
97
|
+
position="absolute"
|
|
98
|
+
left={left}
|
|
99
|
+
top={top}
|
|
100
|
+
width={overlayWidth}
|
|
101
|
+
height={overlayHeight}
|
|
102
|
+
title=" Help "
|
|
103
|
+
titleAlignment="left"
|
|
104
|
+
style={{
|
|
105
|
+
border: true,
|
|
106
|
+
borderColor: colors.textMuted,
|
|
107
|
+
padding: 1,
|
|
108
|
+
flexDirection: "column",
|
|
109
|
+
backgroundColor: colors.surface,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{rows.map((row) => {
|
|
113
|
+
switch (row.kind) {
|
|
114
|
+
case "header":
|
|
115
|
+
return (
|
|
116
|
+
<text
|
|
117
|
+
key={row.key}
|
|
118
|
+
wrapMode="none"
|
|
119
|
+
content={row.text}
|
|
120
|
+
style={{ fg: colors.borderActive, attributes: 1 /* bold */ }}
|
|
121
|
+
/>
|
|
122
|
+
)
|
|
123
|
+
case "footer":
|
|
124
|
+
return (
|
|
125
|
+
<text
|
|
126
|
+
key={row.key}
|
|
127
|
+
wrapMode="none"
|
|
128
|
+
content={row.text}
|
|
129
|
+
style={{ fg: colors.textMuted }}
|
|
130
|
+
/>
|
|
131
|
+
)
|
|
132
|
+
case "spacer":
|
|
133
|
+
return <text key={row.key} content=" " />
|
|
134
|
+
case "binding":
|
|
135
|
+
return (
|
|
136
|
+
<text
|
|
137
|
+
key={row.key}
|
|
138
|
+
wrapMode="none"
|
|
139
|
+
content={row.text}
|
|
140
|
+
style={{ fg: colors.text }}
|
|
141
|
+
/>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
})}
|
|
145
|
+
</box>
|
|
128
146
|
</box>
|
|
129
147
|
)
|
|
130
148
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromptRow — single-line `> query` row shared by the sidebar filter and
|
|
3
|
+
* the command palette query input.
|
|
4
|
+
*
|
|
5
|
+
* Render-only: the parent owns query state and the focus/editing flag.
|
|
6
|
+
* The `> ` prefix always renders in `textStrong` regardless of state so it
|
|
7
|
+
* reads as chrome, not placeholder text — only the body span shifts color
|
|
8
|
+
* (textStrong while editing, text when applied, textMuted as placeholder).
|
|
9
|
+
*
|
|
10
|
+
* Overflow: when editing, an overflowing body anchors its right edge with a
|
|
11
|
+
* leading `…` so the cursor stays on screen; otherwise it anchors the left
|
|
12
|
+
* edge with a trailing `…`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { colors } from "./theme/colors.ts"
|
|
16
|
+
|
|
17
|
+
export interface PromptRowProps {
|
|
18
|
+
readonly query: string
|
|
19
|
+
/** True while the input is focused — shows a cursor and uses textStrong fg. */
|
|
20
|
+
readonly editing: boolean
|
|
21
|
+
/** Body fallback when !editing && query === "". Pass without the `> ` prefix. */
|
|
22
|
+
readonly placeholder?: string
|
|
23
|
+
/** Total cell width available for the row (prefix + body). */
|
|
24
|
+
readonly width: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const PREFIX = "> "
|
|
28
|
+
const CURSOR = "▏"
|
|
29
|
+
|
|
30
|
+
export const PromptRow = ({ query, editing, placeholder = "", width }: PromptRowProps) => {
|
|
31
|
+
const bodyBudget = Math.max(1, width - PREFIX.length)
|
|
32
|
+
|
|
33
|
+
const rawBody = editing ? `${query}${CURSOR}` : query.length > 0 ? query : placeholder
|
|
34
|
+
const bodyFg = editing ? colors.textStrong : query.length > 0 ? colors.text : colors.textMuted
|
|
35
|
+
|
|
36
|
+
const body =
|
|
37
|
+
rawBody.length <= bodyBudget
|
|
38
|
+
? rawBody
|
|
39
|
+
: editing
|
|
40
|
+
? "…" + rawBody.slice(rawBody.length - bodyBudget + 1)
|
|
41
|
+
: rawBody.slice(0, bodyBudget - 1) + "…"
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<text wrapMode="none">
|
|
45
|
+
<span style={{ fg: colors.textStrong }}>{PREFIX}</span>
|
|
46
|
+
<span style={{ fg: bodyFg }}>{body}</span>
|
|
47
|
+
</text>
|
|
48
|
+
)
|
|
49
|
+
}
|
package/src/brand.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
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
CHANGED
|
@@ -25,6 +25,12 @@ export interface ParsedArgs {
|
|
|
25
25
|
readonly configPath: boolean
|
|
26
26
|
/** Value of `--sidebar <mode>` (`auto`, `on`, `off`), or null. Validated by the boot layer. */
|
|
27
27
|
readonly sidebar: string | null
|
|
28
|
+
/** True when `--no-update-check` was passed: suppress the npm-registry
|
|
29
|
+
* probe and the "update available" notice. Mirrors the
|
|
30
|
+
* `NO_UPDATE_NOTIFIER` env var so opt-out is reachable without env state. */
|
|
31
|
+
readonly noUpdateCheck: boolean
|
|
32
|
+
/** True when `--no-mdx` was passed: exclude `.mdx` files from discovery. */
|
|
33
|
+
readonly noMdx: boolean
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/**
|
|
@@ -47,6 +53,8 @@ export const parseArgv = (argv: readonly string[]): ParsedArgs => {
|
|
|
47
53
|
let version = false
|
|
48
54
|
let configPath = false
|
|
49
55
|
let sidebar: string | null = null
|
|
56
|
+
let noUpdateCheck = false
|
|
57
|
+
let noMdx = false
|
|
50
58
|
|
|
51
59
|
for (let i = 0; i < argv.length; i++) {
|
|
52
60
|
const arg = argv[i]!
|
|
@@ -99,13 +107,34 @@ export const parseArgv = (argv: readonly string[]): ParsedArgs => {
|
|
|
99
107
|
}
|
|
100
108
|
continue
|
|
101
109
|
}
|
|
110
|
+
case "--no-update-check":
|
|
111
|
+
noUpdateCheck = true
|
|
112
|
+
continue
|
|
113
|
+
case "--no-mdx":
|
|
114
|
+
noMdx = true
|
|
115
|
+
continue
|
|
102
116
|
}
|
|
103
117
|
if (path === null && !arg.startsWith("-")) {
|
|
104
118
|
path = arg
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
return {
|
|
122
|
+
return {
|
|
123
|
+
path,
|
|
124
|
+
theme,
|
|
125
|
+
tone,
|
|
126
|
+
width,
|
|
127
|
+
all,
|
|
128
|
+
sort,
|
|
129
|
+
serve,
|
|
130
|
+
port,
|
|
131
|
+
help,
|
|
132
|
+
version,
|
|
133
|
+
configPath,
|
|
134
|
+
sidebar,
|
|
135
|
+
noUpdateCheck,
|
|
136
|
+
noMdx,
|
|
137
|
+
}
|
|
109
138
|
}
|
|
110
139
|
|
|
111
140
|
const themeList = themeDefinitions.map((t) => t.id).join(", ")
|
|
@@ -126,9 +155,11 @@ options:
|
|
|
126
155
|
-h, --help show this help and exit
|
|
127
156
|
-v, --version print version and exit
|
|
128
157
|
--config-path print path to the config file and exit
|
|
158
|
+
--no-update-check suppress the "newer version available" check (also via NO_UPDATE_NOTIFIER=1)
|
|
159
|
+
--no-mdx exclude .mdx files from discovery (default: included)
|
|
129
160
|
|
|
130
161
|
configuration:
|
|
131
162
|
file: $XDG_CONFIG_HOME/house/config.toml (default ~/.config/house/config.toml)
|
|
132
|
-
keys: theme, tone
|
|
133
|
-
env: HOUSE_THEME, HOUSE_TONE
|
|
163
|
+
keys: theme, tone, mdx
|
|
164
|
+
env: HOUSE_THEME, HOUSE_TONE, HOUSE_MDX
|
|
134
165
|
precedence (high → low): flags → env → file → defaults`
|
|
@@ -45,6 +45,7 @@ const annotations: Record<string, Annotation> = {
|
|
|
45
45
|
"help.toggle": { title: "Show help", category: "App" },
|
|
46
46
|
"filter.open": { title: "Filter files…", category: "Navigation" },
|
|
47
47
|
"serve.current": { title: "Open in browser", category: "File" },
|
|
48
|
+
"file.edit": { title: "Open in editor", category: "File", keywords: ["editor", "vim", "vscode"] },
|
|
48
49
|
"theme.next": { category: "Appearance" },
|
|
49
50
|
"theme.prev": { category: "Appearance" },
|
|
50
51
|
"theme.toneToggle": { title: "Toggle dark/light tone", category: "Appearance" },
|