@tooee/view 0.1.20 → 0.1.22

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 (52) hide show
  1. package/README.md +28 -0
  2. package/dist/View.d.ts +7 -1
  3. package/dist/View.d.ts.map +1 -1
  4. package/dist/View.js +2 -2
  5. package/dist/View.js.map +1 -1
  6. package/dist/components/subviews/CodeSubview.d.ts.map +1 -1
  7. package/dist/components/subviews/CodeSubview.js +26 -3
  8. package/dist/components/subviews/CodeSubview.js.map +1 -1
  9. package/dist/components/subviews/CustomSubview.d.ts.map +1 -1
  10. package/dist/components/subviews/CustomSubview.js +15 -1
  11. package/dist/components/subviews/CustomSubview.js.map +1 -1
  12. package/dist/components/subviews/MarkdownSubview.d.ts +3 -1
  13. package/dist/components/subviews/MarkdownSubview.d.ts.map +1 -1
  14. package/dist/components/subviews/MarkdownSubview.js +60 -5
  15. package/dist/components/subviews/MarkdownSubview.js.map +1 -1
  16. package/dist/components/subviews/TableSubview.d.ts.map +1 -1
  17. package/dist/components/subviews/TableSubview.js +28 -3
  18. package/dist/components/subviews/TableSubview.js.map +1 -1
  19. package/dist/hooks/index.d.ts +2 -1
  20. package/dist/hooks/index.d.ts.map +1 -1
  21. package/dist/hooks/index.js +1 -1
  22. package/dist/hooks/index.js.map +1 -1
  23. package/dist/hooks/useContentLoader.d.ts.map +1 -1
  24. package/dist/hooks/useContentLoader.js +13 -7
  25. package/dist/hooks/useContentLoader.js.map +1 -1
  26. package/dist/hooks/useViewCommandContext.d.ts +59 -0
  27. package/dist/hooks/useViewCommandContext.d.ts.map +1 -1
  28. package/dist/hooks/useViewCommandContext.js +52 -20
  29. package/dist/hooks/useViewCommandContext.js.map +1 -1
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/launch.d.ts +7 -0
  35. package/dist/launch.d.ts.map +1 -1
  36. package/dist/launch.js +1 -1
  37. package/dist/launch.js.map +1 -1
  38. package/dist/types.d.ts +10 -0
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/types.js.map +1 -1
  41. package/package.json +10 -10
  42. package/src/View.tsx +10 -2
  43. package/src/components/subviews/CodeSubview.tsx +33 -2
  44. package/src/components/subviews/CustomSubview.tsx +18 -1
  45. package/src/components/subviews/MarkdownSubview.tsx +77 -3
  46. package/src/components/subviews/TableSubview.tsx +35 -2
  47. package/src/hooks/index.ts +10 -1
  48. package/src/hooks/useContentLoader.ts +15 -7
  49. package/src/hooks/useViewCommandContext.ts +128 -20
  50. package/src/index.ts +12 -0
  51. package/src/launch.tsx +8 -0
  52. package/src/types.ts +10 -0
@@ -1,4 +1,13 @@
1
1
  export { useContentLoader } from "./useContentLoader.js"
2
2
  export { useMarkState } from "./useMarkState.js"
3
3
  export { useViewCommands } from "./useViewCommands.js"
4
- export { useViewCommandContext } from "./useViewCommandContext.js"
4
+ export {
5
+ createViewCommandContext,
6
+ useProvideViewCommandContext,
7
+ useViewCommandContext,
8
+ } from "./useViewCommandContext.js"
9
+ export type {
10
+ CreateViewCommandContextOptions,
11
+ ProvideViewCommandContextOptions,
12
+ ViewCommandContext,
13
+ } from "./useViewCommandContext.js"
@@ -92,12 +92,18 @@ export function useContentLoader(contentProvider: ContentProvider, reloadTrigger
92
92
  setContent(current)
93
93
  setStreaming(true)
94
94
 
95
+ // Hold the iterator explicitly so cleanup can close it: a cleanup that
96
+ // only flips `cancelled` leaves a pending next() (and the provider's
97
+ // subprocess/file handle/network stream) alive until the next chunk.
98
+ const iterator = loaded[Symbol.asyncIterator]()
99
+
95
100
  ;(async () => {
96
101
  try {
97
- for await (const chunk of loaded) {
98
- if (cancelled) break
102
+ while (true) {
103
+ const result = await iterator.next()
104
+ if (cancelled || result.done) break
105
+ const chunk = result.value
99
106
  if (chunk.type === "marks") {
100
- if (cancelled) break
101
107
  // Streamed mark set: merge by replacing any existing set with same namespace
102
108
  setProviderMarks((prev) => {
103
109
  const filtered = prev.filter((s) => s.namespace !== chunk.set.namespace)
@@ -109,8 +115,8 @@ export function useContentLoader(contentProvider: ContentProvider, reloadTrigger
109
115
  setContent(current)
110
116
  }
111
117
  } catch (err) {
112
- if (!cancelled && err instanceof Error) {
113
- setError(err.message)
118
+ if (!cancelled) {
119
+ setError(err instanceof Error ? err.message : String(err))
114
120
  }
115
121
  } finally {
116
122
  if (!cancelled) {
@@ -121,6 +127,8 @@ export function useContentLoader(contentProvider: ContentProvider, reloadTrigger
121
127
 
122
128
  return () => {
123
129
  cancelled = true
130
+ // Close the iterator to release provider resources mid-stream.
131
+ Promise.resolve(iterator.return?.()).catch(() => {})
124
132
  }
125
133
  }
126
134
 
@@ -130,8 +138,8 @@ export function useContentLoader(contentProvider: ContentProvider, reloadTrigger
130
138
  .then((value) => {
131
139
  if (active) setContent(value)
132
140
  })
133
- .catch((err: Error) => {
134
- if (active) setError(err.message)
141
+ .catch((err: unknown) => {
142
+ if (active) setError(err instanceof Error ? err.message : String(err))
135
143
  })
136
144
  return () => {
137
145
  active = false
@@ -1,8 +1,123 @@
1
- import { useProvideCommandContext, useMode } from "@tooee/commands"
1
+ import { useProvideCommandContextKey, useMode } from "@tooee/commands"
2
+ import type { Mode } from "@tooee/commands"
2
3
  import type { NavigationState } from "@tooee/shell"
3
4
  import type { MarkSet } from "@tooee/marks"
4
5
  import type { AnyContent } from "../types.js"
5
6
 
7
+ /** The `view` field contributed to the command context (see augmentation below). */
8
+ export interface ViewCommandContext {
9
+ content: AnyContent
10
+ format: string
11
+ title?: string
12
+ data?: unknown
13
+ cursor: number | null
14
+ selection: { start: number; end: number } | null
15
+ mode: Mode
16
+ toggledIndices: Set<number>
17
+ reload: () => void
18
+ marks: {
19
+ setMarkSet: (set: MarkSet) => void
20
+ clearNamespace: (namespace: string) => void
21
+ clearAll: () => void
22
+ userMarks: MarkSet[]
23
+ providerMarks: MarkSet[]
24
+ }
25
+ /** Subview extras (e.g. activeRow/selectedRows for tables). */
26
+ [key: string]: unknown
27
+ }
28
+
29
+ declare module "@tooee/commands" {
30
+ interface CommandContext {
31
+ view: ViewCommandContext
32
+ }
33
+ }
34
+
35
+ const noop = () => {}
36
+
37
+ type ViewNavigationContext = Pick<NavigationState, "cursor" | "selection" | "toggledIndices">
38
+
39
+ export interface CreateViewCommandContextOptions {
40
+ /**
41
+ * Content represented by this command context. Custom/headless surfaces may
42
+ * omit it; the builder will synthesize a minimal custom content object.
43
+ */
44
+ content?: AnyContent
45
+ format?: string
46
+ title?: string
47
+ data?: unknown
48
+ nav?: ViewNavigationContext
49
+ cursor?: number | null
50
+ selection?: { start: number; end: number } | null
51
+ /** Caller-owned mode value. Prefer `useProvideViewCommandContext` in React so this is live. */
52
+ mode: Mode
53
+ toggledIndices?: Set<number>
54
+ reload?: () => void
55
+ marks?: Partial<ViewCommandContext["marks"]>
56
+ extras?: Record<string, unknown>
57
+ }
58
+
59
+ export type ProvideViewCommandContextOptions = Omit<CreateViewCommandContextOptions, "mode"> & {
60
+ /** Advanced override; normally omitted so the hook injects the live command mode. */
61
+ mode?: Mode
62
+ }
63
+
64
+ export function createViewCommandContext({
65
+ content,
66
+ format,
67
+ title,
68
+ data,
69
+ nav,
70
+ cursor,
71
+ selection,
72
+ mode,
73
+ toggledIndices,
74
+ reload,
75
+ marks,
76
+ extras,
77
+ }: CreateViewCommandContextOptions): ViewCommandContext {
78
+ const resolvedFormat = format ?? content?.format ?? "custom"
79
+ const resolvedContent: AnyContent =
80
+ content ??
81
+ ({
82
+ format: resolvedFormat,
83
+ data,
84
+ title,
85
+ } satisfies AnyContent)
86
+
87
+ return {
88
+ ...extras,
89
+ content: resolvedContent,
90
+ format: resolvedFormat,
91
+ title: title ?? content?.title,
92
+ data: data ?? ("data" in resolvedContent ? resolvedContent.data : undefined),
93
+ cursor: cursor ?? nav?.cursor ?? null,
94
+ selection: selection ?? nav?.selection ?? null,
95
+ mode,
96
+ toggledIndices: toggledIndices ?? nav?.toggledIndices ?? new Set<number>(),
97
+ reload: reload ?? noop,
98
+ marks: {
99
+ setMarkSet: marks?.setMarkSet ?? noop,
100
+ clearNamespace: marks?.clearNamespace ?? noop,
101
+ clearAll: marks?.clearAll ?? noop,
102
+ userMarks: marks?.userMarks ?? [],
103
+ providerMarks: marks?.providerMarks ?? [],
104
+ },
105
+ }
106
+ }
107
+
108
+ export function useProvideViewCommandContext(
109
+ options: ProvideViewCommandContextOptions | (() => ProvideViewCommandContextOptions),
110
+ ) {
111
+ const mode = useMode()
112
+ useProvideCommandContextKey("view", () => {
113
+ const resolvedOptions = typeof options === "function" ? options() : options
114
+ return createViewCommandContext({
115
+ ...resolvedOptions,
116
+ mode: resolvedOptions.mode ?? mode,
117
+ })
118
+ })
119
+ }
120
+
6
121
  interface UseViewCommandContextParams {
7
122
  content: AnyContent
8
123
  nav: NavigationState
@@ -27,24 +142,17 @@ export function useViewCommandContext({
27
142
  clearAllUserMarks,
28
143
  extras,
29
144
  }: UseViewCommandContextParams) {
30
- const mode = useMode()
31
- useProvideCommandContext(() => ({
32
- view: {
33
- content,
34
- format: content.format,
35
- cursor: nav.cursor,
36
- selection: nav.selection,
37
- mode,
38
- toggledIndices: nav.toggledIndices,
39
- reload,
40
- marks: {
41
- setMarkSet,
42
- clearNamespace: clearMarkNamespace,
43
- clearAll: clearAllUserMarks,
44
- userMarks,
45
- providerMarks,
46
- },
47
- ...extras,
145
+ useProvideViewCommandContext({
146
+ content,
147
+ nav,
148
+ reload,
149
+ marks: {
150
+ setMarkSet,
151
+ clearNamespace: clearMarkNamespace,
152
+ clearAll: clearAllUserMarks,
153
+ userMarks,
154
+ providerMarks,
48
155
  },
49
- }))
156
+ extras,
157
+ })
50
158
  }
package/src/index.ts CHANGED
@@ -10,6 +10,16 @@ export {
10
10
  } from "./default-provider.js"
11
11
  export { listDirectoryFiles } from "./directory-provider.js"
12
12
  export type { DirectoryEntry } from "./directory-provider.js"
13
+ export {
14
+ createViewCommandContext,
15
+ useProvideViewCommandContext,
16
+ useViewCommandContext,
17
+ } from "./hooks/useViewCommandContext.js"
18
+ export type {
19
+ CreateViewCommandContextOptions,
20
+ ProvideViewCommandContextOptions,
21
+ ViewCommandContext,
22
+ } from "./hooks/useViewCommandContext.js"
13
23
  export type {
14
24
  AnyContent,
15
25
  Content,
@@ -25,3 +35,5 @@ export type {
25
35
  TableRow,
26
36
  } from "./types.js"
27
37
  export { getTextContent, isBuiltinContent, isCustomContent } from "./types.js"
38
+ export { CodeBlockChrome } from "@tooee/renderers"
39
+ export type { CodeBlockRenderer, CodeBlockRendererProps, CodeBlockHScroll } from "@tooee/renderers"
package/src/launch.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import { launchCli } from "@tooee/shell"
2
2
  import type { ActionDefinition } from "@tooee/commands"
3
+ import type { CodeBlockRenderer } from "@tooee/renderers"
3
4
  import { View } from "./View.js"
4
5
  import { DirectoryView } from "./DirectoryView.js"
5
6
  import type { ContentProvider, ContentRenderer } from "./types.js"
@@ -8,6 +9,12 @@ export interface ViewLaunchOptions {
8
9
  contentProvider: ContentProvider
9
10
  actions?: ActionDefinition[]
10
11
  renderers?: Record<string, ContentRenderer>
12
+ /**
13
+ * Custom renderers for fenced code blocks in markdown content, keyed by
14
+ * fence type (first word of the fence info string, case-insensitive).
15
+ * Unmatched types fall back to the default syntax-highlighted code block.
16
+ */
17
+ codeBlockRenderers?: Record<string, CodeBlockRenderer>
11
18
  }
12
19
 
13
20
  export interface DirectoryLaunchOptions {
@@ -21,6 +28,7 @@ export async function launch(options: ViewLaunchOptions): Promise<void> {
21
28
  contentProvider={options.contentProvider}
22
29
  actions={options.actions}
23
30
  renderers={options.renderers}
31
+ codeBlockRenderers={options.codeBlockRenderers}
24
32
  />,
25
33
  )
26
34
  }
package/src/types.ts CHANGED
@@ -57,6 +57,16 @@ export interface ContentRendererProps {
57
57
  selectionEnd?: number
58
58
  /** Combined mark state (nav + provider + user marks) */
59
59
  marks?: MarkState
60
+ /**
61
+ * Select a row by index — the mouse equivalent of the keyboard cursor move.
62
+ * A custom renderer that has a notion of rows should call this from its own
63
+ * `onMouseDown`/`onRowClick` handlers to make left-click select a row, the
64
+ * same way the built-in code/markdown/table views do. Selection is a no-op
65
+ * while a modal overlay is open (the host guards it), so renderers can wire
66
+ * it unconditionally. Renderers that own a `RowDocumentRenderable` can map a
67
+ * click's screen-Y to a row via its `getRowAtScreenY(event.y)` helper.
68
+ */
69
+ onSelectRow?: (index: number) => void
60
70
  }
61
71
 
62
72
  export type ContentRenderer = (props: ContentRendererProps) => ReactNode