@carlesandres/house 0.3.0 → 0.3.1
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 +33 -1
- package/README.md +15 -13
- package/package.json +5 -4
- package/src/Browser.tsx +23 -14
- package/src/Footer.tsx +7 -7
- package/src/theme/colors.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to house
|
|
3
|
+
All notable changes to house land here. Format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
|
|
5
5
|
follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) from
|
|
6
6
|
v0.1.0 onward.
|
|
@@ -12,6 +12,38 @@ file is the curated, narrative version.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.3.1] — 2026-05-17
|
|
16
|
+
|
|
17
|
+
Beta release gates (DESIGN §10.2) closed.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Language-tagged fenced code blocks no longer disappear while markdown
|
|
22
|
+
highlighting settles. `house` now uses opentui `0.2.12`, which includes
|
|
23
|
+
markdown/code-block rendering fixes, and the browser pane no longer
|
|
24
|
+
remounts markdown for one file while still holding another file's loaded
|
|
25
|
+
content.
|
|
26
|
+
- Strikethrough text (`~~strike~~`) now renders distinctly (dim + muted
|
|
27
|
+
foreground) instead of as plain body text. opentui's syntax-style API
|
|
28
|
+
has no true strikethrough attribute; this is the closest visual we can
|
|
29
|
+
produce, documented in DESIGN §5.1.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Reduced re-renders and allocations in `Browser` / `Footer` hot paths.
|
|
34
|
+
- DESIGN §10.2 test gate rephrased to target the integration surface
|
|
35
|
+
house owns (the tree-sitter scope map) rather than re-testing opentui's
|
|
36
|
+
renderer; `test/theme-syntax-map.test.ts` enforces scope coverage for
|
|
37
|
+
every node type §5.1.3 promises.
|
|
38
|
+
|
|
39
|
+
### Docs
|
|
40
|
+
|
|
41
|
+
- README embeds a VHS-generated demo gif; `tape/` holds the source
|
|
42
|
+
scripts.
|
|
43
|
+
- `CONTRIBUTING.md` documents the `captureSpans()` / `MockTreeSitterClient`
|
|
44
|
+
/ `TestRecorder` testing patterns and the "before blaming `<markdown>`"
|
|
45
|
+
stale-watcher debugging checklist.
|
|
46
|
+
|
|
15
47
|
## [0.3.0] — 2026-05-16
|
|
16
48
|
|
|
17
49
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
# house
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> is deprecated; install `@carlesandres/house` instead. The CLI binary is
|
|
5
|
-
> now `house` (was `openmdr`).
|
|
6
|
-
|
|
7
|
-
A terminal markdown reader built on [opentui](https://github.com/nicholasgasior/opentui).
|
|
3
|
+
A terminal markdown reader built on [opentui](https://github.com/anomalyco/opentui).
|
|
8
4
|
Point it at a directory and navigate its `.md` files without leaving the terminal.
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
npx @carlesandres/house docs/
|
|
12
|
-
```
|
|
6
|
+

|
|
13
7
|
|
|
14
8
|
Requires [Bun](https://bun.sh) on `PATH`.
|
|
15
9
|
|
|
@@ -52,8 +46,9 @@ house [options] <path>
|
|
|
52
46
|
|-----|--------|
|
|
53
47
|
| `q` / `ctrl+c` | Quit |
|
|
54
48
|
| `tab` | Toggle focus (sidebar ↔ reader) |
|
|
55
|
-
|
|
|
49
|
+
| `s` | Toggle sidebar visibility |
|
|
56
50
|
| `?` | Show / dismiss help overlay |
|
|
51
|
+
| `o` | Open current file in browser as HTML |
|
|
57
52
|
| `t` | Next theme |
|
|
58
53
|
| `T` | Previous theme |
|
|
59
54
|
| `L` | Toggle dark / light tone |
|
|
@@ -64,12 +59,13 @@ house [options] <path>
|
|
|
64
59
|
|-----|--------|
|
|
65
60
|
| `j` / `↓` | Move selection down |
|
|
66
61
|
| `k` / `↑` | Move selection up |
|
|
67
|
-
| `J` | Jump down
|
|
68
|
-
| `K` | Jump up
|
|
69
|
-
| `space` / `ctrl+d` | Page down |
|
|
70
|
-
| `b` / `ctrl+u` | Page up |
|
|
62
|
+
| `J` | Jump down 8 |
|
|
63
|
+
| `K` | Jump up 8 |
|
|
64
|
+
| `space` / `pagedown` / `ctrl+d` | Page down |
|
|
65
|
+
| `b` / `pageup` / `ctrl+u` | Page up |
|
|
71
66
|
| `g` | First file |
|
|
72
67
|
| `G` | Last file |
|
|
68
|
+
| `/` | Filter files (fuzzy match on path) |
|
|
73
69
|
| `↵` / `→` / `l` | Open file (focus reader) |
|
|
74
70
|
|
|
75
71
|
### Reader
|
|
@@ -94,6 +90,12 @@ house [options] <path>
|
|
|
94
90
|
|
|
95
91
|
Each theme supports dark and light tones. Cycle with `t` / `T`; toggle tone with `L`.
|
|
96
92
|
|
|
93
|
+
## Inspiration
|
|
94
|
+
|
|
95
|
+
- [glow](https://github.com/charmbracelet/glow) — render markdown on the CLI, with pizzazz
|
|
96
|
+
- [ghui](https://github.com/kitlangton/ghui) — keyboard-driven terminal UI for GitHub pull requests
|
|
97
|
+
- [hunk](https://github.com/modem-dev/hunk) — review-first terminal diff viewer for agent-authored changesets
|
|
98
|
+
|
|
97
99
|
## License
|
|
98
100
|
|
|
99
101
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carlesandres/house",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "TUI-first markdown reader on opentui",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,12 +44,13 @@
|
|
|
44
44
|
"format": "oxfmt src/",
|
|
45
45
|
"format:check": "oxfmt --check src/",
|
|
46
46
|
"test": "bun test",
|
|
47
|
-
"build:themes": "bun run dev/build-themes.ts"
|
|
47
|
+
"build:themes": "bun run dev/build-themes.ts",
|
|
48
|
+
"repro:pty-codeblocks": "bun run dev/repro-pty-codeblocks.ts"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
50
51
|
"@effect/atom-react": "4.0.0-beta.59",
|
|
51
|
-
"@opentui/core": "0.2.
|
|
52
|
-
"@opentui/react": "0.2.
|
|
52
|
+
"@opentui/core": "0.2.12",
|
|
53
|
+
"@opentui/react": "0.2.12",
|
|
53
54
|
"effect": "4.0.0-beta.59",
|
|
54
55
|
"ignore": "^7.0.5",
|
|
55
56
|
"marked": "^18.0.3",
|
package/src/Browser.tsx
CHANGED
|
@@ -14,7 +14,7 @@ import { SyntaxStyle } from "@opentui/core"
|
|
|
14
14
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
15
15
|
import { useAtomValue, useAtomSet } from "@effect/atom-react"
|
|
16
16
|
import { Effect } from "effect"
|
|
17
|
-
import { useEffect, useMemo, useRef, useState } from "react"
|
|
17
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
18
18
|
import { filterFiles } from "./discovery/filter.ts"
|
|
19
19
|
import { type FileEntry } from "./discovery/walk.ts"
|
|
20
20
|
import { Footer, FOOTER_HEIGHT } from "./Footer.tsx"
|
|
@@ -69,7 +69,7 @@ export const Browser = ({
|
|
|
69
69
|
const [selectedIndex, setSelectedIndex] = useState(() =>
|
|
70
70
|
clamp(initialIndex, 0, Math.max(0, files.length - 1)),
|
|
71
71
|
)
|
|
72
|
-
const [
|
|
72
|
+
const [loaded, setLoaded] = useState<{ path: string; content: string } | null>(null)
|
|
73
73
|
const [error, setError] = useState<string | null>(null)
|
|
74
74
|
const [focus, setFocus] = useState<"sidebar" | "reader">("sidebar")
|
|
75
75
|
const [sidebarVisible, setSidebarVisible] = useState<boolean>(true)
|
|
@@ -121,7 +121,6 @@ export const Browser = ({
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
const displayedFiles = useMemo(() => filterFiles(files, filterQuery), [files, filterQuery])
|
|
124
|
-
|
|
125
124
|
// When the filtered list shrinks, keep selectedIndex valid. The reset to 0
|
|
126
125
|
// on every query change happens in the keystroke handler, not here, so a
|
|
127
126
|
// no-op rerender doesn't snap the cursor back to the top.
|
|
@@ -145,24 +144,24 @@ export const Browser = ({
|
|
|
145
144
|
if (target === renderedPath) return
|
|
146
145
|
const timer = setTimeout(() => setRenderedPath(target), 80)
|
|
147
146
|
return () => clearTimeout(timer)
|
|
148
|
-
}, [selected, renderedPath])
|
|
147
|
+
}, [selected?.path, renderedPath])
|
|
149
148
|
|
|
150
149
|
useEffect(() => {
|
|
151
150
|
if (!renderedPath) {
|
|
152
|
-
|
|
151
|
+
setLoaded(null)
|
|
153
152
|
return
|
|
154
153
|
}
|
|
155
154
|
let cancelled = false
|
|
156
155
|
readFile(renderedPath).then(
|
|
157
156
|
(text) => {
|
|
158
157
|
if (!cancelled) {
|
|
159
|
-
|
|
158
|
+
setLoaded({ path: renderedPath, content: text })
|
|
160
159
|
setError(null)
|
|
161
160
|
}
|
|
162
161
|
},
|
|
163
162
|
(err: unknown) => {
|
|
164
163
|
if (!cancelled) {
|
|
165
|
-
|
|
164
|
+
setLoaded(null)
|
|
166
165
|
setError(`Cannot read ${renderedPath}: ${String(err)}`)
|
|
167
166
|
}
|
|
168
167
|
},
|
|
@@ -330,6 +329,7 @@ export const Browser = ({
|
|
|
330
329
|
const sidebarTitle = sidebarActive ? " ▸ files " : " files "
|
|
331
330
|
const readerLabel = selected?.relativePath ?? title
|
|
332
331
|
const readerTitle = readerActive ? ` ▸ ${readerLabel} ` : ` ${readerLabel} `
|
|
332
|
+
const content = loaded?.path === renderedPath ? loaded.content : ""
|
|
333
333
|
|
|
334
334
|
// Sidebar virtualization: render only the visible window. Without this,
|
|
335
335
|
// every keystroke re-renders all N file rows even though only the bg of
|
|
@@ -352,16 +352,25 @@ export const Browser = ({
|
|
|
352
352
|
const sidebarTextWidth = Math.max(4, sidebarWidth - 2)
|
|
353
353
|
// Right-anchored truncation: keep the filename visible, lose the prefix
|
|
354
354
|
// with a leading ellipsis when the path is too long.
|
|
355
|
-
const truncatePath = (
|
|
356
|
-
s
|
|
355
|
+
const truncatePath = useCallback(
|
|
356
|
+
(s: string): string =>
|
|
357
|
+
s.length <= sidebarTextWidth ? s : "…" + s.slice(s.length - sidebarTextWidth + 1),
|
|
358
|
+
[sidebarTextWidth],
|
|
359
|
+
)
|
|
357
360
|
|
|
358
361
|
// While help is open, the `?` key closes the overlay — relabel its hint
|
|
359
362
|
// so the footer accurately describes what pressing the key will do.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
// Memoized: `helpVisible` changes rarely; `browserBindings` and
|
|
364
|
+
// `HELP_ALLOWED_IDS` are module-level constants.
|
|
365
|
+
const footerBindings = useMemo(
|
|
366
|
+
() =>
|
|
367
|
+
helpVisible
|
|
368
|
+
? browserBindings
|
|
369
|
+
.filter((b) => HELP_ALLOWED_IDS.has(b.id))
|
|
370
|
+
.map((b) => (b.id === "help.toggle" ? { ...b, hint: "close" } : b))
|
|
371
|
+
: browserBindings,
|
|
372
|
+
[helpVisible],
|
|
373
|
+
)
|
|
365
374
|
|
|
366
375
|
return (
|
|
367
376
|
<box style={{ width, height, flexDirection: "column", backgroundColor: colors.background }}>
|
package/src/Footer.tsx
CHANGED
|
@@ -126,13 +126,13 @@ export const Footer = <C,>({ bindings, ctx, width, notice, filter }: FooterProps
|
|
|
126
126
|
)
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
)
|
|
129
|
+
const hints: string[] = []
|
|
130
|
+
for (const b of bindings) {
|
|
131
|
+
if (b.when && !b.when(ctx)) continue
|
|
132
|
+
const h = formatHint(b)
|
|
133
|
+
if (h !== null) hints.push(h)
|
|
134
|
+
}
|
|
135
|
+
const hintContent = fitHints(hints, usableWidth)
|
|
136
136
|
const noticeContent = notice
|
|
137
137
|
? notice.length > usableWidth
|
|
138
138
|
? notice.slice(0, usableWidth)
|
package/src/theme/colors.ts
CHANGED
|
@@ -50,6 +50,9 @@ const buildSyntaxMap = (r: ResolvedTheme): Record<string, StyleDefinitionInput>
|
|
|
50
50
|
"markup.bold": { fg: r.markdownStrong, bold: true },
|
|
51
51
|
"markup.strong": { fg: r.markdownStrong, bold: true },
|
|
52
52
|
"markup.italic": { fg: r.markdownEmph, italic: true },
|
|
53
|
+
// opentui's SyntaxStyle has no strikethrough attribute — best-effort:
|
|
54
|
+
// dim + muted color so `~~strike~~` is at least visually distinguishable.
|
|
55
|
+
"markup.strikethrough": { fg: r.textMuted, dim: true },
|
|
53
56
|
"markup.list": { fg: r.markdownListItem },
|
|
54
57
|
"markup.quote": { fg: r.markdownBlockQuote, italic: true },
|
|
55
58
|
"markup.raw": { fg: r.markdownCode, bg: codeBg },
|