@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to house (renamed from `openmdr` in 0.3.0) land here. Format follows
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
- > **Renamed from `openmdr`.** The old npm package `@carlesandres/openmdr`
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
+ ![house demo](tape/house.gif)
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
- | `\` | Toggle sidebar visibility |
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 10 |
68
- | `K` | Jump up 10 |
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.0",
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.1",
52
- "@opentui/react": "0.2.1",
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 [content, setContent] = useState<string>("")
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
- setContent("")
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
- setContent(text)
158
+ setLoaded({ path: renderedPath, content: text })
160
159
  setError(null)
161
160
  }
162
161
  },
163
162
  (err: unknown) => {
164
163
  if (!cancelled) {
165
- setContent("")
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 = (s: string): string =>
356
- s.length <= sidebarTextWidth ? s : "…" + s.slice(s.length - sidebarTextWidth + 1)
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
- const footerBindings = helpVisible
361
- ? browserBindings
362
- .filter((b) => HELP_ALLOWED_IDS.has(b.id))
363
- .map((b) => (b.id === "help.toggle" ? { ...b, hint: "close" } : b))
364
- : browserBindings
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 hintContent = fitHints(
130
- bindings
131
- .filter((b) => (b.when ? b.when(ctx) : true))
132
- .map(formatHint)
133
- .filter((s): s is string => s !== null),
134
- usableWidth,
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)
@@ -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 },