@tooee/view 0.1.11 → 0.1.12

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 (84) hide show
  1. package/dist/DirectoryView.js.map +1 -1
  2. package/dist/View.d.ts.map +1 -1
  3. package/dist/View.js +41 -353
  4. package/dist/View.js.map +1 -1
  5. package/dist/components/SubviewLayout.d.ts +15 -0
  6. package/dist/components/SubviewLayout.d.ts.map +1 -0
  7. package/dist/components/SubviewLayout.js +26 -0
  8. package/dist/components/SubviewLayout.js.map +1 -0
  9. package/dist/components/subviews/CodeSubview.d.ts +8 -0
  10. package/dist/components/subviews/CodeSubview.d.ts.map +1 -0
  11. package/dist/components/subviews/CodeSubview.js +71 -0
  12. package/dist/components/subviews/CodeSubview.js.map +1 -0
  13. package/dist/components/subviews/CustomSubview.d.ts +9 -0
  14. package/dist/components/subviews/CustomSubview.d.ts.map +1 -0
  15. package/dist/components/subviews/CustomSubview.js +87 -0
  16. package/dist/components/subviews/CustomSubview.js.map +1 -0
  17. package/dist/components/subviews/ImageSubview.d.ts +8 -0
  18. package/dist/components/subviews/ImageSubview.d.ts.map +1 -0
  19. package/dist/components/subviews/ImageSubview.js +32 -0
  20. package/dist/components/subviews/ImageSubview.js.map +1 -0
  21. package/dist/components/subviews/MarkdownSubview.d.ts +8 -0
  22. package/dist/components/subviews/MarkdownSubview.d.ts.map +1 -0
  23. package/dist/components/subviews/MarkdownSubview.js +80 -0
  24. package/dist/components/subviews/MarkdownSubview.js.map +1 -0
  25. package/dist/components/subviews/TableSubview.d.ts +8 -0
  26. package/dist/components/subviews/TableSubview.d.ts.map +1 -0
  27. package/dist/components/subviews/TableSubview.js +113 -0
  28. package/dist/components/subviews/TableSubview.js.map +1 -0
  29. package/dist/components/subviews/index.d.ts +8 -0
  30. package/dist/components/subviews/index.d.ts.map +1 -0
  31. package/dist/components/subviews/index.js +7 -0
  32. package/dist/components/subviews/index.js.map +1 -0
  33. package/dist/components/subviews/types.d.ts +15 -0
  34. package/dist/components/subviews/types.d.ts.map +1 -0
  35. package/dist/components/subviews/types.js +2 -0
  36. package/dist/components/subviews/types.js.map +1 -0
  37. package/dist/hooks/index.d.ts +5 -0
  38. package/dist/hooks/index.d.ts.map +1 -0
  39. package/dist/hooks/index.js +5 -0
  40. package/dist/hooks/index.js.map +1 -0
  41. package/dist/hooks/useContentLoader.d.ts +10 -0
  42. package/dist/hooks/useContentLoader.d.ts.map +1 -0
  43. package/dist/hooks/useContentLoader.js +125 -0
  44. package/dist/hooks/useContentLoader.js.map +1 -0
  45. package/dist/hooks/useMarkState.d.ts +13 -0
  46. package/dist/hooks/useMarkState.d.ts.map +1 -0
  47. package/dist/hooks/useMarkState.js +71 -0
  48. package/dist/hooks/useMarkState.js.map +1 -0
  49. package/dist/hooks/useViewCommandContext.d.ts +18 -0
  50. package/dist/hooks/useViewCommandContext.d.ts.map +1 -0
  51. package/dist/hooks/useViewCommandContext.js +23 -0
  52. package/dist/hooks/useViewCommandContext.js.map +1 -0
  53. package/dist/hooks/useViewCommands.d.ts +13 -0
  54. package/dist/hooks/useViewCommands.d.ts.map +1 -0
  55. package/dist/hooks/useViewCommands.js +20 -0
  56. package/dist/hooks/useViewCommands.js.map +1 -0
  57. package/dist/index.d.ts +1 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +1 -1
  60. package/dist/index.js.map +1 -1
  61. package/dist/launch.d.ts.map +1 -1
  62. package/dist/launch.js.map +1 -1
  63. package/dist/types.d.ts +8 -0
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/types.js.map +1 -1
  66. package/package.json +24 -23
  67. package/src/DirectoryView.tsx +1 -1
  68. package/src/View.tsx +49 -462
  69. package/src/components/SubviewLayout.tsx +55 -0
  70. package/src/components/subviews/CodeSubview.tsx +112 -0
  71. package/src/components/subviews/CustomSubview.tsx +139 -0
  72. package/src/components/subviews/ImageSubview.tsx +64 -0
  73. package/src/components/subviews/MarkdownSubview.tsx +122 -0
  74. package/src/components/subviews/TableSubview.tsx +153 -0
  75. package/src/components/subviews/index.ts +7 -0
  76. package/src/components/subviews/types.ts +15 -0
  77. package/src/hooks/index.ts +4 -0
  78. package/src/hooks/useContentLoader.ts +147 -0
  79. package/src/hooks/useMarkState.ts +99 -0
  80. package/src/hooks/useViewCommandContext.ts +49 -0
  81. package/src/hooks/useViewCommands.ts +36 -0
  82. package/src/index.ts +6 -1
  83. package/src/launch.tsx +1 -6
  84. package/src/types.ts +9 -0
@@ -0,0 +1,147 @@
1
+ import { useState, useEffect } from "react"
2
+ import type { MarkSet } from "@tooee/marks"
3
+ import type {
4
+ AnyContent,
5
+ Content,
6
+ ContentFormat,
7
+ ContentProvider,
8
+ ContentChunk,
9
+ CustomContent,
10
+ } from "../types.js"
11
+
12
+ function isAsyncIterable(value: unknown): value is AsyncIterable<ContentChunk> {
13
+ return value != null && typeof value === "object" && Symbol.asyncIterator in value
14
+ }
15
+
16
+ function createEmptyContent(format: string, title?: string): AnyContent {
17
+ switch (format) {
18
+ case "markdown":
19
+ return { format, markdown: "", title }
20
+ case "code":
21
+ return { format, code: "", title }
22
+ case "text":
23
+ return { format, text: "", title }
24
+ case "image":
25
+ return { format, src: "", title }
26
+ case "table":
27
+ return { format, columns: [], rows: [], title }
28
+ default:
29
+ return { format, data: undefined, title } as CustomContent
30
+ }
31
+ }
32
+
33
+ function ensureContentFormat<F extends ContentFormat>(
34
+ current: AnyContent | null,
35
+ format: F,
36
+ title?: string,
37
+ ): Extract<Content, { format: F }> {
38
+ if (!current || current.format !== format) {
39
+ return createEmptyContent(format, title) as Extract<Content, { format: F }>
40
+ }
41
+ return current as Extract<Content, { format: F }>
42
+ }
43
+
44
+ function applyContentChunk(
45
+ current: AnyContent | null,
46
+ chunk: ContentChunk,
47
+ title?: string,
48
+ ): AnyContent {
49
+ switch (chunk.type) {
50
+ case "replace":
51
+ return chunk.content
52
+ case "append":
53
+ if (chunk.format === "markdown") {
54
+ const target = ensureContentFormat(current, "markdown", title)
55
+ return { ...target, markdown: target.markdown + chunk.data }
56
+ }
57
+ if (chunk.format === "code") {
58
+ const target = ensureContentFormat(current, "code", title)
59
+ return {
60
+ ...target,
61
+ code: target.code + chunk.data,
62
+ language: chunk.language ?? target.language,
63
+ }
64
+ }
65
+ {
66
+ const target = ensureContentFormat(current, "text", title)
67
+ return { ...target, text: target.text + chunk.data }
68
+ }
69
+ case "patch":
70
+ return chunk.apply(current)
71
+ default:
72
+ return current ?? createEmptyContent("markdown", title)
73
+ }
74
+ }
75
+
76
+ export function useContentLoader(contentProvider: ContentProvider, reloadTrigger: number) {
77
+ const [content, setContent] = useState<AnyContent | null>(null)
78
+ const [streaming, setStreaming] = useState(false)
79
+ const [error, setError] = useState<string | null>(null)
80
+ const [providerMarks, setProviderMarks] = useState<MarkSet[]>([])
81
+
82
+ useEffect(() => {
83
+ setError(null)
84
+ setStreaming(false)
85
+ // Reset provider marks on reload; initialize with static marks if provided
86
+ setProviderMarks(contentProvider.marks ?? [])
87
+ const loaded = contentProvider.load()
88
+
89
+ if (isAsyncIterable(loaded)) {
90
+ const fallbackFormat = contentProvider.format ?? "markdown"
91
+ const title = contentProvider.title
92
+ let cancelled = false
93
+ let current: AnyContent | null = createEmptyContent(fallbackFormat, title)
94
+ setContent(current)
95
+ setStreaming(true)
96
+
97
+ ;(async () => {
98
+ try {
99
+ for await (const chunk of loaded) {
100
+ if (cancelled) break
101
+ if (chunk.type === "marks") {
102
+ if (cancelled) break
103
+ // Streamed mark set: merge by replacing any existing set with same namespace
104
+ setProviderMarks((prev) => {
105
+ const filtered = prev.filter((s) => s.namespace !== chunk.set.namespace)
106
+ return [...filtered, chunk.set]
107
+ })
108
+ continue
109
+ }
110
+ current = applyContentChunk(current, chunk, title)
111
+ setContent(current)
112
+ }
113
+ } catch (err) {
114
+ if (!cancelled && err instanceof Error) {
115
+ setError(err.message)
116
+ }
117
+ } finally {
118
+ if (!cancelled) {
119
+ setStreaming(false)
120
+ }
121
+ }
122
+ })()
123
+
124
+ return () => {
125
+ cancelled = true
126
+ }
127
+ }
128
+
129
+ if (loaded instanceof Promise) {
130
+ let active = true
131
+ loaded
132
+ .then((value) => {
133
+ if (active) setContent(value)
134
+ })
135
+ .catch((err: Error) => {
136
+ if (active) setError(err.message)
137
+ })
138
+ return () => {
139
+ active = false
140
+ }
141
+ }
142
+
143
+ setContent(loaded)
144
+ }, [contentProvider, reloadTrigger])
145
+
146
+ return { content, streaming, error, providerMarks, setProviderMarks }
147
+ }
@@ -0,0 +1,99 @@
1
+ import { useMemo } from "react"
2
+ import { MarkSetBuilder, createMarkState, MarkPriorities } from "@tooee/marks"
3
+ import type { MarkSet, MarkState } from "@tooee/marks"
4
+ import type { NavigationState, SearchState } from "@tooee/shell"
5
+ import type { ResolvedTheme } from "@tooee/themes"
6
+
7
+ interface UseMarkStateParams {
8
+ nav: Pick<NavigationState, "cursor" | "selection" | "toggledIndices">
9
+ search: Pick<SearchState, "matchingLines" | "currentMatchIndex">
10
+ theme: ResolvedTheme
11
+ providerMarks: MarkSet[]
12
+ userMarks: MarkSet[]
13
+ }
14
+
15
+ export function useMarkState({
16
+ nav,
17
+ search,
18
+ theme,
19
+ providerMarks,
20
+ userMarks,
21
+ }: UseMarkStateParams): MarkState | undefined {
22
+ return useMemo(() => {
23
+ const sets: MarkSet[] = []
24
+
25
+ // Search matches
26
+ if (search.matchingLines.length > 0) {
27
+ const mapped = new Set(search.matchingLines)
28
+ const builder = new MarkSetBuilder()
29
+ for (const idx of mapped) {
30
+ builder.addLine(idx, {
31
+ background: theme.warning,
32
+ signBefore: "\u25CF",
33
+ foreground: theme.warning,
34
+ })
35
+ }
36
+ sets.push(builder.build("search", MarkPriorities.SEARCH_MATCH))
37
+ }
38
+
39
+ // Toggled lines
40
+ if (nav.toggledIndices.size > 0) {
41
+ const mapped = new Set(nav.toggledIndices)
42
+ const builder = new MarkSetBuilder()
43
+ for (const idx of mapped) {
44
+ builder.addLine(idx, { background: theme.backgroundPanel })
45
+ }
46
+ sets.push(builder.build("toggled", MarkPriorities.TOGGLED))
47
+ }
48
+
49
+ // Selection range
50
+ if (nav.selection) {
51
+ const builder = new MarkSetBuilder()
52
+ builder.addRange(
53
+ { line: nav.selection.start.line },
54
+ { line: nav.selection.end.line },
55
+ { background: theme.selection },
56
+ )
57
+ sets.push(builder.build("selection", MarkPriorities.SELECTION))
58
+ }
59
+
60
+ // Current match highlight
61
+ if (search.matchingLines.length > 0) {
62
+ const currentLine = search.matchingLines[search.currentMatchIndex]
63
+ if (currentLine != null) {
64
+ const builder = new MarkSetBuilder()
65
+ builder.addLine(currentLine, {
66
+ background: theme.primary,
67
+ signBefore: "\u25CF",
68
+ foreground: theme.primary,
69
+ })
70
+ sets.push(builder.build("currentMatch", MarkPriorities.CURRENT_MATCH))
71
+ }
72
+ }
73
+
74
+ // Cursor
75
+ if (nav.cursor) {
76
+ const builder = new MarkSetBuilder()
77
+ builder.addLine(nav.cursor.line, {
78
+ background: theme.cursorLine,
79
+ signBefore: "\u25B8",
80
+ foreground: theme.primary,
81
+ })
82
+ sets.push(builder.build("cursor", MarkPriorities.CURSOR))
83
+ }
84
+
85
+ // Provider and user marks
86
+ sets.push(...providerMarks, ...userMarks)
87
+
88
+ return sets.length > 0 ? createMarkState(sets) : undefined
89
+ }, [
90
+ search.matchingLines,
91
+ search.currentMatchIndex,
92
+ nav.toggledIndices,
93
+ nav.selection,
94
+ nav.cursor,
95
+ theme,
96
+ providerMarks,
97
+ userMarks,
98
+ ])
99
+ }
@@ -0,0 +1,49 @@
1
+ import { useProvideCommandContext } from "@tooee/commands"
2
+ import type { NavigationState } from "@tooee/shell"
3
+ import type { MarkSet } from "@tooee/marks"
4
+ import type { AnyContent } from "../types.js"
5
+
6
+ interface UseViewCommandContextParams {
7
+ content: AnyContent
8
+ nav: NavigationState
9
+ reload: () => void
10
+ providerMarks: MarkSet[]
11
+ userMarks: MarkSet[]
12
+ setMarkSet: (set: MarkSet) => void
13
+ clearMarkNamespace: (namespace: string) => void
14
+ clearAllUserMarks: () => void
15
+ /** Extra fields merged into the view context (e.g. activeRow, selectedRows for table) */
16
+ extras?: Record<string, unknown>
17
+ }
18
+
19
+ export function useViewCommandContext({
20
+ content,
21
+ nav,
22
+ reload,
23
+ providerMarks,
24
+ userMarks,
25
+ setMarkSet,
26
+ clearMarkNamespace,
27
+ clearAllUserMarks,
28
+ extras,
29
+ }: UseViewCommandContextParams) {
30
+ useProvideCommandContext(() => ({
31
+ view: {
32
+ content,
33
+ format: content.format,
34
+ cursor: nav.cursor,
35
+ selection: nav.selection,
36
+ mode: nav.mode,
37
+ toggledIndices: nav.toggledIndices,
38
+ reload,
39
+ marks: {
40
+ setMarkSet,
41
+ clearNamespace: clearMarkNamespace,
42
+ clearAll: clearAllUserMarks,
43
+ userMarks,
44
+ providerMarks,
45
+ },
46
+ ...extras,
47
+ },
48
+ }))
49
+ }
@@ -0,0 +1,36 @@
1
+ import { useState } from "react"
2
+ import {
3
+ useThemeCommands,
4
+ useQuitCommand,
5
+ useCopyCommand,
6
+ useToggleLineNumbersCommand,
7
+ } from "@tooee/shell"
8
+ import { useActions } from "@tooee/commands"
9
+ import type { ActionDefinition } from "@tooee/commands"
10
+ import { useConfig } from "@tooee/config"
11
+ import type { AnyContent } from "../types.js"
12
+
13
+ interface UseViewCommandsParams {
14
+ content: AnyContent | null
15
+ textContent: string
16
+ actions?: ActionDefinition[]
17
+ }
18
+
19
+ export function useViewCommands({ content, textContent, actions }: UseViewCommandsParams) {
20
+ const config = useConfig()
21
+ const [showLineNumbers, setShowLineNumbers] = useState(config.view?.gutter ?? true)
22
+
23
+ const { name: themeName } = useThemeCommands()
24
+ useQuitCommand()
25
+ useCopyCommand({
26
+ getText: () => (content ? textContent : undefined),
27
+ })
28
+ useToggleLineNumbersCommand({
29
+ showLineNumbers,
30
+ onToggle: () => setShowLineNumbers((v) => !v),
31
+ })
32
+
33
+ useActions(actions)
34
+
35
+ return { themeName, showLineNumbers }
36
+ }
package/src/index.ts CHANGED
@@ -2,7 +2,12 @@ export { View } from "./View.js"
2
2
  export { DirectoryView } from "./DirectoryView.js"
3
3
  export { launch, launchDirectory } from "./launch.js"
4
4
  export type { ViewLaunchOptions } from "./launch.js"
5
- export { createFileProvider, createStdinProvider, createTableFileProvider, createTableStdinProvider } from "./default-provider.js"
5
+ export {
6
+ createFileProvider,
7
+ createStdinProvider,
8
+ createTableFileProvider,
9
+ createTableStdinProvider,
10
+ } from "./default-provider.js"
6
11
  export { listDirectoryFiles } from "./directory-provider.js"
7
12
  export type { DirectoryEntry } from "./directory-provider.js"
8
13
  export type {
package/src/launch.tsx CHANGED
@@ -26,10 +26,5 @@ export async function launch(options: ViewLaunchOptions): Promise<void> {
26
26
  }
27
27
 
28
28
  export async function launchDirectory(options: DirectoryLaunchOptions): Promise<void> {
29
- await launchCli(
30
- <DirectoryView
31
- dirPath={options.dirPath}
32
- actions={options.actions}
33
- />,
34
- )
29
+ await launchCli(<DirectoryView dirPath={options.dirPath} actions={options.actions} />)
35
30
  }
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from "react"
2
2
  import type { ColumnDef, TableRow } from "@tooee/renderers"
3
+ import type { MarkSet, MarkState } from "@tooee/marks"
3
4
 
4
5
  // === Built-in content types ===
5
6
 
@@ -59,6 +60,8 @@ export interface ContentRendererProps {
59
60
  cursor?: number
60
61
  selectionStart?: number
61
62
  selectionEnd?: number
63
+ /** Combined mark state (nav + provider + user marks) */
64
+ marks?: MarkState
62
65
  }
63
66
 
64
67
  export type ContentRenderer = (props: ContentRendererProps) => ReactNode
@@ -82,6 +85,10 @@ export type ContentChunk =
82
85
  type: "patch"
83
86
  apply: (current: AnyContent | null) => AnyContent
84
87
  }
88
+ | {
89
+ type: "marks"
90
+ set: MarkSet
91
+ }
85
92
 
86
93
  // === Provider ===
87
94
 
@@ -89,6 +96,8 @@ export interface ContentProvider {
89
96
  load(): AnyContent | Promise<AnyContent> | AsyncIterable<ContentChunk>
90
97
  format?: string
91
98
  title?: string
99
+ /** Static marks to apply to the content. Merged with nav marks in View. */
100
+ marks?: MarkSet[]
92
101
  }
93
102
 
94
103
  // === Compat aliases ===